- CompanyView.vue: hero, plattform, produkte, nyx CTA, footer - ImpressumView.vue + DatenschutzView.vue: legal pages - Router: HTML5 history mode (no # URLs), company routes - Reverted vite-ssg (SSR compat needs proper refactor, planned) - Removed ssr-shim.ts Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2325 lines
97 KiB
JavaScript
2325 lines
97 KiB
JavaScript
import { ref, triggerRef, watch, computed, nextTick, onUnmounted, defineComponent, mergeProps, useSSRContext, createSlots, withCtx, openBlock, createBlock, createCommentVNode, toDisplayString, Fragment, renderList, createVNode, unref, createTextVNode, resolveDynamicComponent, withModifiers, onUpdated, toRef, onMounted, resolveComponent } from "vue";
|
||
import { ssrRenderAttrs, ssrRenderSlot, ssrRenderComponent, ssrRenderAttr, ssrInterpolate, ssrRenderList, ssrRenderVNode, ssrRenderClass, ssrIncludeBooleanAttr, ssrRenderStyle } from "vue/server-renderer";
|
||
import { useRouter, useRoute } from "vue-router";
|
||
import { u as useChatStore, w as ws, a as agents, g as getApiBase, b as auth, _ as _export_sfc, c as useTtsPlayer, d as useUI, e as agentLogo, s as scrollbarOptions } from "../main.mjs";
|
||
import { marked } from "marked";
|
||
import { OverlayScrollbarsComponent } from "overlayscrollbars-vue";
|
||
import { BoltIcon, ComputerDesktopIcon, CpuChipIcon, GlobeAltIcon, DocumentPlusIcon, WrenchIcon, PencilIcon, BookOpenIcon, ChatBubbleLeftIcon, LinkIcon, Cog6ToothIcon, SpeakerWaveIcon, ExclamationTriangleIcon, ChevronDownIcon, InformationCircleIcon, LockClosedIcon, UserGroupIcon, ChatBubbleBottomCenterTextIcon, PaperClipIcon, MicrophoneIcon, StopIcon, ArrowUpIcon } from "@heroicons/vue/20/solid";
|
||
import "@unhead/vue/server";
|
||
import "pinia";
|
||
import "@heroicons/vue/24/outline";
|
||
import "overlayscrollbars";
|
||
const VISIBLE_PAGE$1 = 50;
|
||
const MAX_HUD_NODES = 100;
|
||
function useSessionHistory(isAgentRunning, visibleCount, agentIdFn) {
|
||
const store = useChatStore();
|
||
const sessionHistoryComplete = ref(false);
|
||
const lastUsage = ref(null);
|
||
let loadStartTime = null;
|
||
let pendingMessages = [];
|
||
let pendingUsageTotals = null;
|
||
const lastSystemMsgRef = ref(null);
|
||
const hudTree = ref([]);
|
||
const hudVersion = ref(0);
|
||
const hudPending = /* @__PURE__ */ new Map();
|
||
const hudTurns = /* @__PURE__ */ new Map();
|
||
const toolCallMap = /* @__PURE__ */ new Map();
|
||
function lookupByToolCallId(toolCallId) {
|
||
if (!toolCallId) return null;
|
||
const ref2 = toolCallMap.get(toolCallId);
|
||
if (!ref2) return null;
|
||
const node = ref2.deref();
|
||
if (!node) {
|
||
toolCallMap.delete(toolCallId);
|
||
return null;
|
||
}
|
||
return node;
|
||
}
|
||
const activeTurnCorrId = ref(null);
|
||
function makeNode(partial) {
|
||
return {
|
||
id: partial.id || crypto.randomUUID(),
|
||
type: partial.type || "received",
|
||
state: partial.state || "running",
|
||
label: partial.label || "",
|
||
children: [],
|
||
replay: partial.replay ?? false,
|
||
startedAt: partial.startedAt || Date.now(),
|
||
...partial
|
||
};
|
||
}
|
||
function findNode(nodes, corrId) {
|
||
for (const n of nodes) {
|
||
if (n.correlationId === corrId) return n;
|
||
if (n.children) {
|
||
const found = findNode(n.children, corrId);
|
||
if (found) return found;
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
function addHudNode(node, parentCorrelationId) {
|
||
if (parentCorrelationId && parentCorrelationId !== "history") {
|
||
const parent = hudTurns.get(parentCorrelationId);
|
||
if (parent) {
|
||
parent.children.push(node);
|
||
triggerRef(hudTree);
|
||
hudVersion.value++;
|
||
return;
|
||
}
|
||
}
|
||
hudTree.value.unshift(node);
|
||
if (hudTree.value.length > MAX_HUD_NODES) hudTree.value.splice(MAX_HUD_NODES);
|
||
triggerRef(hudTree);
|
||
hudVersion.value++;
|
||
}
|
||
function pushHudEvent(event) {
|
||
const replay = !!event.replay;
|
||
const ts = event.ts || Date.now();
|
||
const corrId = event.correlationId;
|
||
const parentId = event.parentId;
|
||
switch (event.event) {
|
||
case "turn_start": {
|
||
if (corrId && (hudPending.has(corrId) || hudTurns.has(corrId))) break;
|
||
const node = makeNode({
|
||
type: "turn",
|
||
state: "running",
|
||
label: "🔄 Turn",
|
||
correlationId: corrId,
|
||
startedAt: ts,
|
||
replay
|
||
});
|
||
if (corrId) {
|
||
hudPending.set(corrId, node);
|
||
hudTurns.set(corrId, node);
|
||
}
|
||
if (!replay && corrId) activeTurnCorrId.value = corrId;
|
||
addHudNode(node);
|
||
break;
|
||
}
|
||
case "turn_end": {
|
||
const node = corrId ? hudPending.get(corrId) : null;
|
||
if (node) {
|
||
node.state = "done";
|
||
node.endedAt = ts;
|
||
node.durationMs = event.durationMs;
|
||
if (corrId) {
|
||
hudPending.delete(corrId);
|
||
hudTurns.delete(corrId);
|
||
}
|
||
triggerRef(hudTree);
|
||
hudVersion.value++;
|
||
}
|
||
if (!replay && corrId && activeTurnCorrId.value === corrId) activeTurnCorrId.value = null;
|
||
break;
|
||
}
|
||
case "think_start": {
|
||
const node = makeNode({
|
||
type: "think",
|
||
state: "running",
|
||
label: "Thinking",
|
||
correlationId: corrId,
|
||
startedAt: ts,
|
||
replay
|
||
});
|
||
if (corrId) hudPending.set(corrId, node);
|
||
addHudNode(node, parentId);
|
||
break;
|
||
}
|
||
case "think_end": {
|
||
const node = corrId ? hudPending.get(corrId) : null;
|
||
if (node) {
|
||
node.state = "done";
|
||
node.endedAt = ts;
|
||
node.durationMs = event.durationMs;
|
||
if (corrId) hudPending.delete(corrId);
|
||
triggerRef(hudTree);
|
||
hudVersion.value++;
|
||
} else {
|
||
addHudNode(makeNode({ type: "think", state: "done", label: "Thinking", correlationId: corrId, startedAt: ts, endedAt: ts, durationMs: event.durationMs, replay }), parentId);
|
||
}
|
||
break;
|
||
}
|
||
case "tool_start": {
|
||
const tool = event.tool || "unknown";
|
||
const args = event.args || {};
|
||
const label = buildToolLabel(tool, args);
|
||
const node = makeNode({
|
||
type: "tool",
|
||
state: "running",
|
||
label,
|
||
tool,
|
||
args,
|
||
correlationId: corrId,
|
||
startedAt: ts,
|
||
replay
|
||
});
|
||
if (corrId) hudPending.set(corrId, node);
|
||
if (event.toolCallId) toolCallMap.set(event.toolCallId, new WeakRef(node));
|
||
addHudNode(node, parentId);
|
||
lastSystemMsgRef.value = label;
|
||
break;
|
||
}
|
||
case "tool_end": {
|
||
const tool = event.tool || "unknown";
|
||
const result = event.result || {};
|
||
let node = lookupByToolCallId(event.toolCallId) ?? (corrId ? hudPending.get(corrId) : null);
|
||
if (!node && parentId) {
|
||
const turnNode = findNode(hudTree.value, parentId);
|
||
if (turnNode?.children) {
|
||
const match = turnNode.children.find(
|
||
(n) => n.type === "tool" && n.state === "running" && (n.tool === tool || n.tool === "unknown")
|
||
);
|
||
if (match) {
|
||
node = match;
|
||
if (match.correlationId) hudPending.delete(match.correlationId);
|
||
}
|
||
}
|
||
}
|
||
if (node) {
|
||
node.state = result.ok === false ? "error" : "done";
|
||
node.result = result;
|
||
node.endedAt = ts;
|
||
node.durationMs = event.durationMs;
|
||
node.label = buildToolLabel(tool, node.args || {}, result);
|
||
if (corrId) hudPending.delete(corrId);
|
||
if (event.toolCallId) toolCallMap.delete(event.toolCallId);
|
||
triggerRef(hudTree);
|
||
hudVersion.value++;
|
||
} else {
|
||
const label = buildToolLabel(tool, event.args || {}, result);
|
||
addHudNode(makeNode({ type: "tool", state: "done", label, tool, result, correlationId: corrId, startedAt: ts, endedAt: ts, durationMs: event.durationMs, replay }), parentId);
|
||
}
|
||
break;
|
||
}
|
||
case "received": {
|
||
const node = makeNode({
|
||
type: "received",
|
||
state: "done",
|
||
subtype: event.subtype,
|
||
label: event.label || event.subtype || "received",
|
||
startedAt: ts,
|
||
endedAt: ts,
|
||
replay
|
||
});
|
||
addHudNode(node);
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
function buildToolLabel(tool, args, result) {
|
||
const fileTools = ["read", "write", "edit", "append"];
|
||
if (fileTools.includes(tool)) {
|
||
const vp = args.viewerPath || args.path || "";
|
||
const filename = vp.split("/").pop() || vp;
|
||
const area = result?.area || args.area;
|
||
const areaStr = area ? `:L${area.startLine}–${area.endLine}` : "";
|
||
return `${filename}${areaStr}`;
|
||
}
|
||
if (tool === "exec") {
|
||
const cmd = args.command || "";
|
||
return `${cmd.slice(0, 60)}${cmd.length > 60 ? "…" : ""}`;
|
||
}
|
||
if (tool === "web_fetch") return (args.url || "").slice(0, 60);
|
||
if (tool === "web_search") return (args.query || "").slice(0, 60);
|
||
return tool;
|
||
}
|
||
function getToolsForTurn(corrId) {
|
||
if (!corrId) return [];
|
||
const turn = hudTree.value.find((n) => n.correlationId === corrId) ?? [...hudTurns.values()].find((n) => n.correlationId === corrId);
|
||
return turn ? turn.children.filter((c) => c.type === "tool") : [];
|
||
}
|
||
function hudSnapshot() {
|
||
const lines = [`HUD tree — ${hudTree.value.length} root node(s)
|
||
`];
|
||
for (const node of hudTree.value) {
|
||
const dur = node.durationMs != null ? ` [${node.durationMs}ms]` : "";
|
||
const repl = node.replay ? " (replay)" : "";
|
||
lines.push(` ${node.state === "running" ? "⏳" : node.state === "error" ? "❌" : "✅"} [${node.type}] ${node.label}${dur}${repl}`);
|
||
lines.push(` id=${node.id.slice(0, 8)} corrId=${(node.correlationId || "—").slice(0, 8)} children=${node.children.length}`);
|
||
for (const child of node.children) {
|
||
const cdur = child.durationMs != null ? ` [${child.durationMs}ms]` : "";
|
||
lines.push(` ${child.state === "running" ? "⏳" : child.state === "error" ? "❌" : "✅"} [${child.type}] ${child.label}${cdur}`);
|
||
if (child.args) lines.push(` args: ${JSON.stringify(child.args).slice(0, 80)}`);
|
||
if (child.result) lines.push(` result: ${JSON.stringify(child.result).slice(0, 80)}`);
|
||
}
|
||
}
|
||
return lines.join("\n");
|
||
}
|
||
function pushSystem(text) {
|
||
lastSystemMsgRef.value = text;
|
||
}
|
||
function flushPendingClear(pendingClearRef) {
|
||
if (!pendingClearRef.value) return;
|
||
pendingClearRef.value = false;
|
||
store.clearMessages();
|
||
visibleCount.value = VISIBLE_PAGE$1;
|
||
sessionHistoryComplete.value = false;
|
||
loadStartTime = performance.now();
|
||
pendingMessages = [];
|
||
pendingUsageTotals = null;
|
||
}
|
||
function revealMessages() {
|
||
loadStartTime = null;
|
||
const _pending = pendingMessages;
|
||
const _usage = pendingUsageTotals;
|
||
pendingMessages = [];
|
||
pendingUsageTotals = null;
|
||
const idx = store.messages.findIndex((m) => m.role === "system" && m.content.includes("Loading session history..."));
|
||
if (idx !== -1) {
|
||
store.messages.splice(idx, 1, ..._pending);
|
||
} else {
|
||
store.messages.unshift(..._pending);
|
||
}
|
||
const revealedCount = _pending.filter((m) => m.role !== "system").length;
|
||
store.sessionContextHint = revealedCount > 0 ? `${revealedCount} msgs in context` : "fresh context";
|
||
if (_usage) lastUsage.value = _usage;
|
||
}
|
||
function handleSessionHistory(entries) {
|
||
if (!entries?.length) return;
|
||
if (loadStartTime === null) loadStartTime = performance.now();
|
||
if (!store.messages.some((m) => m.content?.includes("Loading session history..."))) {
|
||
store.pushSystem("⏳ Loading session history...", agentIdFn());
|
||
}
|
||
const newMsgs = [];
|
||
const currentAgentId = agentIdFn();
|
||
const currentSessionId = store.localSessionId;
|
||
let pendingUsage = null;
|
||
for (const data of entries) {
|
||
if (data.type === "hud") {
|
||
pushHudEvent({ ...data, replay: true });
|
||
continue;
|
||
}
|
||
if (data.event === "tool_start" || data.event === "tool_end" || data.event === "think_start" || data.event === "think_end" || data.event === "turn_start" || data.event === "turn_end" || data.event === "received") {
|
||
pushHudEvent({ ...data, replay: true });
|
||
continue;
|
||
}
|
||
if (data.entry_type === "session_context") {
|
||
newMsgs.push({ role: "session_context", content: data.content || "", agentId: currentAgentId, sessionId: currentSessionId });
|
||
} else if (data.entry_type === "user_message") {
|
||
newMsgs.push({ role: "user", content: data.content || "", agentId: currentAgentId, sessionId: currentSessionId });
|
||
} else if (data.entry_type === "assistant_text") {
|
||
const content = (data.content || "").replace(/^\[\[reply_to[^\]]*\]\]\s*/i, "").trim();
|
||
if (!content) continue;
|
||
const msg = { role: "assistant", content, streaming: false, agentId: currentAgentId, sessionId: currentSessionId, timestamp: data.ts || null };
|
||
if (data.truncated) msg.truncated = true;
|
||
if (pendingUsage) {
|
||
msg.usage = pendingUsage;
|
||
pendingUsage = null;
|
||
}
|
||
newMsgs.push(msg);
|
||
} else if (data.entry_type === "usage") {
|
||
pendingUsage = {
|
||
input_tokens: data.input_tokens || 0,
|
||
output_tokens: data.output_tokens || 0,
|
||
total_tokens: data.total_tokens || 0,
|
||
cost: Number(data.cost || 0)
|
||
};
|
||
const last = newMsgs[newMsgs.length - 1];
|
||
if (last?.role === "assistant") {
|
||
last.usage = pendingUsage;
|
||
pendingUsage = null;
|
||
}
|
||
}
|
||
}
|
||
pendingMessages = newMsgs;
|
||
const totalUsage = entries.filter((e) => e.entry_type === "usage").reduce((acc, e) => ({
|
||
input_tokens: acc.input_tokens + (e.input_tokens || 0),
|
||
output_tokens: acc.output_tokens + (e.output_tokens || 0),
|
||
total_tokens: acc.total_tokens + (e.total_tokens || 0),
|
||
cost: acc.cost + Number(e.cost || 0)
|
||
}), { input_tokens: 0, output_tokens: 0, total_tokens: 0, cost: 0 });
|
||
pendingUsageTotals = totalUsage.total_tokens > 0 ? totalUsage : null;
|
||
}
|
||
function handleSessionEntry(data, sentMessages, pushSystemFn) {
|
||
if (data.type === "hud") {
|
||
pushHudEvent(data);
|
||
return;
|
||
}
|
||
const isReplay = !sessionHistoryComplete.value;
|
||
const currentAgentId = agentIdFn();
|
||
const currentSessionId = store.localSessionId;
|
||
switch (data.entry_type) {
|
||
case "user_message": {
|
||
const raw = data.content || "";
|
||
if (raw.startsWith("A new session was started")) break;
|
||
if (!isReplay && raw.includes("[voice transcript]:")) break;
|
||
const hasByMsgId = data.msgId && store.messages.some((m) => m.msgId === data.msgId);
|
||
if (hasByMsgId) break;
|
||
if (!isReplay && !sentMessages.has(raw.trim())) {
|
||
store.messages.push({ role: "user", content: raw, agentId: currentAgentId, sessionId: currentSessionId, msgId: data.msgId });
|
||
} else {
|
||
sentMessages.delete(raw.trim());
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
function resetHudMaps() {
|
||
hudPending.clear();
|
||
hudTurns.clear();
|
||
toolCallMap.clear();
|
||
activeTurnCorrId.value = null;
|
||
hudTree.value = [];
|
||
hudVersion.value++;
|
||
}
|
||
function toolCallMapSnapshot() {
|
||
return [...toolCallMap.entries()].map(([k, ref2]) => {
|
||
const node = ref2.deref();
|
||
return { toolCallId: k, label: node?.label ?? null, state: node?.state ?? null, stale: !node };
|
||
});
|
||
}
|
||
return {
|
||
sessionHistoryComplete,
|
||
lastUsage,
|
||
lastSystemMsgRef,
|
||
hudTree,
|
||
hudVersion,
|
||
activeTurnCorrId,
|
||
getToolsForTurn,
|
||
pushHudEvent,
|
||
hudSnapshot,
|
||
toolCallMapSnapshot,
|
||
resetHudMaps,
|
||
flushPendingClear,
|
||
revealMessages,
|
||
handleSessionHistory,
|
||
handleSessionEntry,
|
||
pushSystem
|
||
};
|
||
}
|
||
function useAgentSocket(visibleCount, lastUsage, pendingClearRef, sentMessages, restoreLastSent) {
|
||
const chatStore = useChatStore();
|
||
const { send: wsSend, onMessage: onWsMessage, replayBuffer } = ws;
|
||
const { updateFromServer, selectedAgent } = agents;
|
||
const handoverInProgress = () => chatStore.smState === "HANDOVER_PENDING" || chatStore.smState === "HANDOVER_DONE";
|
||
const isAgentRunning = () => chatStore.smState === "AGENT_RUNNING";
|
||
const history = useSessionHistory(isAgentRunning, visibleCount, () => selectedAgent.value);
|
||
history.lastUsage = lastUsage;
|
||
watch(history.activeTurnCorrId, (id) => {
|
||
chatStore.activeTurnCorrId = id;
|
||
});
|
||
function pushSystem(text) {
|
||
history.pushSystem(text);
|
||
}
|
||
function mount() {
|
||
const handlers = {
|
||
auth_ok(data) {
|
||
updateFromServer(data);
|
||
},
|
||
ready(data) {
|
||
updateFromServer(data);
|
||
if (data.session_id) chatStore.sessionKey = data.session_id;
|
||
else if (data.sessionId) chatStore.sessionKey = data.sessionId;
|
||
chatStore.applyConnectionState("SYNCED");
|
||
chatStore.applyChannelState("READY");
|
||
},
|
||
// assay session info — store session ID for reconnect
|
||
session_info(data) {
|
||
if (data.session_id) chatStore.sessionKey = data.session_id;
|
||
},
|
||
// assay UI controls — store for later rendering
|
||
controls(_data) {
|
||
},
|
||
// assay artifacts — store for later rendering
|
||
artifacts(_data) {
|
||
},
|
||
// assay session cleared
|
||
cleared(_data) {
|
||
chatStore.messages.splice(0);
|
||
},
|
||
thinking(data) {
|
||
if (!handoverInProgress()) chatStore.appendThinking(data.content);
|
||
},
|
||
delta(data) {
|
||
if (!handoverInProgress()) {
|
||
history.flushPendingClear(pendingClearRef);
|
||
chatStore.collapseThinking();
|
||
chatStore.appendAssistantDelta(data.content, data.agentId);
|
||
}
|
||
},
|
||
message(data) {
|
||
if (handoverInProgress()) return;
|
||
history.flushPendingClear(pendingClearRef);
|
||
if (data.streaming === false) {
|
||
chatStore.createCompleteAssistantMessage(data.content, data.agentId, data.usage);
|
||
} else if (data.final) {
|
||
chatStore.finalizeAssistantMessage(null, data.usage);
|
||
}
|
||
},
|
||
truncated_warning(_data) {
|
||
chatStore.collapseThinking();
|
||
if (chatStore.hasActiveStreamingMessage()) chatStore.finalizeAssistantMessage(null, void 0, true);
|
||
chatStore.truncatedWarning = true;
|
||
},
|
||
done(data) {
|
||
if (handoverInProgress()) return;
|
||
chatStore.collapseThinking();
|
||
if (data.suppress) {
|
||
chatStore.suppressAssistantMessage();
|
||
} else {
|
||
const doneContent = data.content || null;
|
||
if (chatStore.hasActiveStreamingMessage()) {
|
||
const deltaLen = chatStore.streamingMessageLength();
|
||
const useContent = doneContent && deltaLen < doneContent.length ? doneContent : null;
|
||
chatStore.finalizeAssistantMessage(useContent, data.usage);
|
||
} else if (doneContent) {
|
||
chatStore.createCompleteAssistantMessage(doneContent, void 0, data.usage);
|
||
}
|
||
}
|
||
history.lastSystemMsgRef.value = null;
|
||
},
|
||
session_history(data) {
|
||
if (chatStore.hasActiveStreamingMessage()) chatStore.finalizeAssistantMessage(null);
|
||
chatStore.collapseThinking();
|
||
history.flushPendingClear(pendingClearRef);
|
||
history.handleSessionHistory(data.entries);
|
||
},
|
||
hud(data) {
|
||
history.pushHudEvent(data);
|
||
if (data.event === "turn_start" && !data.replay) {
|
||
chatStore.activeTurnCorrId = data.correlationId ?? null;
|
||
chatStore.startNewAssistantMessage(selectedAgent.value);
|
||
}
|
||
},
|
||
event(data) {
|
||
if (data.event === "agent") {
|
||
const stream = data.payload?.stream;
|
||
if (stream === "tool") {
|
||
console.log("[HUD agent/tool]", JSON.stringify(data.payload).slice(0, 400));
|
||
}
|
||
}
|
||
},
|
||
tool(data) {
|
||
if (data.action === "call") {
|
||
pushSystem(`${data.tool} ${data.args || ""}`);
|
||
} else if (data.action === "result") {
|
||
pushSystem(`→ ${data.result || ""}`);
|
||
}
|
||
},
|
||
session_entry(data) {
|
||
history.handleSessionEntry(data, sentMessages, pushSystem);
|
||
},
|
||
message_update(data) {
|
||
if (!data.msgId || !data.patch) return;
|
||
const patch = { ...data.patch };
|
||
if (patch.voiceAudioUrl) {
|
||
const token = localStorage.getItem("nyx_session") || localStorage.getItem("titan_token") || "";
|
||
patch.voiceAudioUrl = `${patch.voiceAudioUrl}?token=${encodeURIComponent(token)}`;
|
||
}
|
||
if (patch.transcript) {
|
||
sentMessages.add(`[voice transcript]: ${patch.transcript}`.trim());
|
||
}
|
||
const patched = chatStore.patchMessage(data.msgId, patch);
|
||
if (!patched) {
|
||
console.warn("[message_update] no message found for msgId:", data.msgId);
|
||
}
|
||
},
|
||
handover_done(data) {
|
||
chatStore.messages.push({
|
||
role: "assistant",
|
||
content: data.content || "📋 Handover written.",
|
||
agentId: selectedAgent.value,
|
||
sessionId: chatStore.localSessionId
|
||
});
|
||
chatStore.messages.push({
|
||
role: "system",
|
||
content: "Start a new session with this handover context?",
|
||
confirmNew: true,
|
||
confirmed: false,
|
||
agentId: selectedAgent.value,
|
||
sessionId: chatStore.localSessionId
|
||
});
|
||
},
|
||
handover_context(_data) {
|
||
},
|
||
// New two-SM protocol: channel_state + connection_state
|
||
channel_state(data) {
|
||
if (!data.state) return;
|
||
if (data.clear_history) {
|
||
console.log("[clear] channel switch", { state: data.state, msgCount: chatStore.messages.length });
|
||
history.resetHudMaps();
|
||
chatStore.messages.length = 0;
|
||
pendingClearRef.value = false;
|
||
}
|
||
const prevChannel = chatStore.channelState;
|
||
chatStore.applyChannelState(data.state);
|
||
if (data.state === "READY" || data.state === "FRESH") {
|
||
history.lastSystemMsgRef.value = null;
|
||
if (chatStore.queuedThought !== null && prevChannel === "AGENT_RUNNING") {
|
||
const thought = chatStore.queuedThought;
|
||
chatStore.queuedThought = null;
|
||
wsSend({ type: "message", content: thought });
|
||
}
|
||
}
|
||
},
|
||
connection_state(data) {
|
||
if (!data.state) return;
|
||
chatStore.applyConnectionState(data.state);
|
||
if (data.state === "LOADING_HISTORY") {
|
||
history.resetHudMaps();
|
||
chatStore.messages.length = 0;
|
||
pendingClearRef.value = false;
|
||
}
|
||
if (data.state === "SYNCED") {
|
||
history.flushPendingClear(pendingClearRef);
|
||
history.revealMessages();
|
||
}
|
||
},
|
||
// Legacy: still handle session_state for backward compat
|
||
session_state(data) {
|
||
if (!data.state) return;
|
||
if (data.reconnected) ;
|
||
if (data.reconnected || data.clear_history) {
|
||
console.log("[clear] immediate flush", { reconnected: data.reconnected, clear_history: data.clear_history, state: data.state, msgCount: chatStore.messages.length });
|
||
chatStore.stashMessages();
|
||
history.resetHudMaps();
|
||
chatStore.messages.length = 0;
|
||
pendingClearRef.value = false;
|
||
}
|
||
chatStore.applySessionState(data.state);
|
||
if (data.state === "READY" || data.state === "FRESH" || data.state === "IDLE") {
|
||
history.lastSystemMsgRef.value = null;
|
||
if (chatStore.queuedThought !== null) {
|
||
const thought = chatStore.queuedThought;
|
||
chatStore.queuedThought = null;
|
||
wsSend({ type: "message", content: thought });
|
||
}
|
||
}
|
||
},
|
||
session_total_tokens(data) {
|
||
chatStore.sessionTotalTokens = data;
|
||
},
|
||
finance_update(data) {
|
||
chatStore.finance = data;
|
||
},
|
||
usage(data) {
|
||
if (!handoverInProgress()) {
|
||
chatStore.sessionTotalTokens = {
|
||
input_tokens: data.input_tokens || (chatStore.sessionTotalTokens?.input_tokens || 0),
|
||
cache_read_tokens: data.cache_read_tokens || (chatStore.sessionTotalTokens?.cache_read_tokens || 0),
|
||
output_tokens: data.output_tokens || (chatStore.sessionTotalTokens?.output_tokens || 0)
|
||
};
|
||
}
|
||
},
|
||
session_status(data) {
|
||
if (data.status === "no_session") {
|
||
pendingClearRef.value = true;
|
||
history.flushPendingClear(pendingClearRef);
|
||
chatStore.messages.push({
|
||
role: "system",
|
||
type: "no_session",
|
||
content: "-- NO SESSION --",
|
||
agentId: selectedAgent.value,
|
||
sessionId: chatStore.localSessionId
|
||
});
|
||
chatStore.sessionContextHint = "";
|
||
} else if (data.status === "watching") {
|
||
history.flushPendingClear(pendingClearRef);
|
||
history.sessionHistoryComplete.value = true;
|
||
history.revealMessages();
|
||
}
|
||
},
|
||
sent(_data) {
|
||
},
|
||
switch_ok(data) {
|
||
history.sessionHistoryComplete.value = false;
|
||
if (data.sessionKey) chatStore.sessionKey = data.sessionKey;
|
||
},
|
||
new_ok(_data) {
|
||
history.sessionHistoryComplete.value = false;
|
||
},
|
||
error(data) {
|
||
if (data.code === "SESSION_TERMINATED") {
|
||
chatStore.pushSystem("⚠️ Message not delivered — session was resetting. Please try again.", selectedAgent.value);
|
||
restoreLastSent?.();
|
||
} else if (data.code === "DISCARDED_NOT_IDLE" || data.code === "DISCARDED_NOT_READY") {
|
||
chatStore.pushSystem("⚠️ Message not delivered — agent was busy. Please try again.", selectedAgent.value);
|
||
restoreLastSent?.();
|
||
}
|
||
},
|
||
stopped(_data) {
|
||
chatStore.pushSystem("✅ Agent stopped", selectedAgent.value);
|
||
},
|
||
killed(_data) {
|
||
chatStore.pushSystem("☠️ Agent killed", selectedAgent.value);
|
||
}
|
||
};
|
||
const unsubscribe = onWsMessage((data) => {
|
||
const handler = handlers[data.type];
|
||
if (handler) handler(data);
|
||
});
|
||
replayBuffer((data) => {
|
||
const handler = handlers[data.type];
|
||
if (handler) handler(data);
|
||
});
|
||
return unsubscribe;
|
||
}
|
||
return {
|
||
mount,
|
||
lastSystemMsg: history.lastSystemMsgRef,
|
||
hudTree: history.hudTree,
|
||
hudVersion: history.hudVersion,
|
||
getToolsForTurn: history.getToolsForTurn,
|
||
hudSnapshot: history.hudSnapshot,
|
||
toolCallMapSnapshot: history.toolCallMapSnapshot,
|
||
sessionHistoryComplete: history.sessionHistoryComplete,
|
||
pushSystem,
|
||
hasActiveStreamingMessage: chatStore.hasActiveStreamingMessage
|
||
};
|
||
}
|
||
function generateMsgId() {
|
||
return crypto.randomUUID();
|
||
}
|
||
const renderer = new marked.Renderer();
|
||
renderer.link = ({ href, title, text }) => {
|
||
const titleAttr = title ? ` title="${title}"` : "";
|
||
return `<a href="${href}"${titleAttr} target="_blank" rel="noopener noreferrer">${text}</a>`;
|
||
};
|
||
function ansiToHtml(text) {
|
||
const colorMap = {
|
||
30: "#555",
|
||
31: "#e06c75",
|
||
32: "#98c379",
|
||
33: "#e5c07b",
|
||
34: "#61afef",
|
||
35: "#c678dd",
|
||
36: "#56b6c2",
|
||
37: "#abb2bf"
|
||
};
|
||
let open = false, bold = false, dim = false;
|
||
const result = text.replace(/\x1b\[([0-9;]*)m/g, (_match, codes) => {
|
||
const parts = codes.split(";").map(Number);
|
||
let out = "";
|
||
for (const code of parts) {
|
||
if (code === 0) {
|
||
if (open) {
|
||
out += "</span>";
|
||
open = false;
|
||
}
|
||
if (bold) {
|
||
out += "</strong>";
|
||
bold = false;
|
||
}
|
||
if (dim) {
|
||
out += "</span>";
|
||
dim = false;
|
||
}
|
||
} else if (code === 1) {
|
||
if (!bold) {
|
||
out += "<strong>";
|
||
bold = true;
|
||
}
|
||
} else if (code === 2) {
|
||
if (!dim) {
|
||
out += '<span style="opacity:0.45">';
|
||
dim = true;
|
||
}
|
||
} else if (colorMap[code]) {
|
||
if (open) {
|
||
out += "</span>";
|
||
}
|
||
out += `<span style="color:${colorMap[code]}">`;
|
||
open = true;
|
||
}
|
||
}
|
||
return out;
|
||
});
|
||
let tail = "";
|
||
if (open) tail += "</span>";
|
||
if (bold) tail += "</strong>";
|
||
if (dim) tail += "</span>";
|
||
return result + tail;
|
||
}
|
||
const WORKSPACE_PATH_RE = /(workspace\/[^\s"'<>)]+\.(?:pdf|png|jpg|jpeg|gif|csv|json|txt|md|html|zip|mp3|wav|ogg|webm|m4a))/g;
|
||
const WORKSPACE_PREFIX = "";
|
||
const AUDIO_EXTENSIONS = /* @__PURE__ */ new Set(["mp3", "wav", "ogg", "webm", "m4a"]);
|
||
function linkifyWorkspaceFiles(html) {
|
||
return html.replace(WORKSPACE_PATH_RE, (match) => {
|
||
const name = match.split("/").pop() || match;
|
||
const absPath = match.startsWith("/") ? match : WORKSPACE_PREFIX + match;
|
||
const ext = name.split(".").pop()?.toLowerCase() || "";
|
||
if (AUDIO_EXTENSIONS.has(ext)) {
|
||
return `<audio controls class="inline-audio" data-filepath="${absPath}" onplay="window.__hermesAudioSrc(this)"></audio>`;
|
||
}
|
||
return `<button class="file-download-link" data-filepath="${absPath}" data-filename="${name}" onclick="window.__hermesDownload(this)" title="Download ${name}">📎 ${name}</button>`;
|
||
});
|
||
}
|
||
if (typeof window !== "undefined" && !window.__hermesAudioSrc) {
|
||
window.__hermesAudioSrc = (el) => {
|
||
if (el.src) return;
|
||
const filepath = el.dataset.filepath;
|
||
if (!filepath) return;
|
||
const token = localStorage.getItem("nyx_session") || localStorage.getItem("titan_token") || "";
|
||
const apiBase = getApiBase();
|
||
el.src = `${apiBase}/api/files${filepath}?token=${encodeURIComponent(token)}`;
|
||
};
|
||
}
|
||
if (typeof window !== "undefined" && !window.__hermesDownload) {
|
||
window.__hermesDownload = async (el) => {
|
||
const filepath = el.dataset.filepath;
|
||
const filename = el.dataset.filename || "download";
|
||
if (!filepath) return;
|
||
el.textContent = "⏳ " + filename;
|
||
try {
|
||
const token = localStorage.getItem("nyx_session") || localStorage.getItem("titan_token") || "";
|
||
const apiBase2 = getApiBase();
|
||
const res = await fetch(`${apiBase2}/api/files${filepath}?token=${encodeURIComponent(token)}`);
|
||
if (!res.ok) throw new Error(`${res.status}`);
|
||
const blob = await res.blob();
|
||
const url = URL.createObjectURL(blob);
|
||
const a = document.createElement("a");
|
||
a.href = url;
|
||
a.download = filename;
|
||
a.click();
|
||
URL.revokeObjectURL(url);
|
||
el.textContent = "✅ " + filename;
|
||
} catch (err) {
|
||
el.textContent = "❌ " + filename;
|
||
console.error("[download]", err);
|
||
}
|
||
};
|
||
}
|
||
function parseMd(content) {
|
||
const raw = content || "";
|
||
if (/\x1b\[/.test(raw)) {
|
||
const escaped = raw.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
||
return `<pre style="font-family:var(--font-mono);line-height:1.5;white-space:pre-wrap">${ansiToHtml(escaped)}</pre>`;
|
||
}
|
||
let html = marked.parse(raw, { renderer, async: false, gfm: true, breaks: true });
|
||
html = linkifyWorkspaceFiles(html);
|
||
return html;
|
||
}
|
||
const DRAFT_KEY = "chat_draft";
|
||
const HISTORY_KEY = "chat_input_history";
|
||
const HISTORY_MAX = 50;
|
||
function loadDraft() {
|
||
try {
|
||
return sessionStorage.getItem(DRAFT_KEY) || "";
|
||
} catch {
|
||
return "";
|
||
}
|
||
}
|
||
function saveDraft(v) {
|
||
try {
|
||
if (v) sessionStorage.setItem(DRAFT_KEY, v);
|
||
else sessionStorage.removeItem(DRAFT_KEY);
|
||
} catch {
|
||
}
|
||
}
|
||
function loadHistory() {
|
||
try {
|
||
return JSON.parse(sessionStorage.getItem(HISTORY_KEY) || "[]");
|
||
} catch {
|
||
return [];
|
||
}
|
||
}
|
||
function pushHistory(v) {
|
||
try {
|
||
const h = loadHistory().filter((x) => x !== v);
|
||
h.unshift(v);
|
||
sessionStorage.setItem(HISTORY_KEY, JSON.stringify(h.slice(0, HISTORY_MAX)));
|
||
} catch {
|
||
}
|
||
}
|
||
function useMessages(wsSendFn) {
|
||
const store = useChatStore();
|
||
const sending = ref(false);
|
||
const input = ref(loadDraft());
|
||
const messagesEl = ref(null);
|
||
let historyIdx = -1;
|
||
function getViewport() {
|
||
const el = messagesEl.value;
|
||
if (!el) return null;
|
||
const root = el.$el || el;
|
||
return root.querySelector?.("[data-overlayscrollbars-viewport]") || root;
|
||
}
|
||
function scrollToBottom() {
|
||
nextTick(() => {
|
||
nextTick(() => {
|
||
requestAnimationFrame(() => {
|
||
const vp = getViewport();
|
||
if (vp) vp.scrollTop = vp.scrollHeight;
|
||
});
|
||
});
|
||
});
|
||
}
|
||
function scrollIfAtBottom() {
|
||
const vp = getViewport();
|
||
if (!vp) return;
|
||
if (vp.scrollHeight - vp.scrollTop - vp.clientHeight < 80) scrollToBottom();
|
||
}
|
||
let draftTimer = null;
|
||
function onInputChange() {
|
||
if (draftTimer) clearTimeout(draftTimer);
|
||
draftTimer = setTimeout(() => saveDraft(input.value), 1e3);
|
||
}
|
||
function navigateHistory(dir) {
|
||
const history = loadHistory();
|
||
if (!history.length) return;
|
||
if (dir === "up") {
|
||
historyIdx = Math.min(historyIdx + 1, history.length - 1);
|
||
} else {
|
||
historyIdx = Math.max(historyIdx - 1, -1);
|
||
}
|
||
input.value = historyIdx === -1 ? "" : history[historyIdx];
|
||
}
|
||
let lastSentContent = "";
|
||
function restoreLastSent() {
|
||
input.value = lastSentContent;
|
||
}
|
||
async function send(attachmentPayload) {
|
||
const hasText = input.value.trim().length > 0;
|
||
const hasAttachments = attachmentPayload && attachmentPayload.length > 0;
|
||
if (!hasText && !hasAttachments || sending.value) return;
|
||
const content = input.value.trim();
|
||
if (hasText) {
|
||
lastSentContent = content;
|
||
pushHistory(content);
|
||
}
|
||
historyIdx = -1;
|
||
input.value = "";
|
||
saveDraft("");
|
||
sending.value = true;
|
||
const msgId = generateMsgId();
|
||
const localAttachments = hasAttachments ? attachmentPayload.map((a) => {
|
||
const mime = a.mimeType.split(";")[0];
|
||
let dataUrl;
|
||
if (a.mimeType.startsWith("audio/")) {
|
||
const bytes = Uint8Array.from(atob(a.content), (c) => c.charCodeAt(0));
|
||
dataUrl = URL.createObjectURL(new Blob([bytes], { type: mime }));
|
||
} else {
|
||
dataUrl = `data:${mime};base64,${a.content}`;
|
||
}
|
||
return { mimeType: a.mimeType, fileName: a.fileName, dataUrl };
|
||
}) : void 0;
|
||
const hasAudio = hasAttachments && attachmentPayload.some((a) => a.mimeType.startsWith("audio/"));
|
||
store.pushMessage({
|
||
role: "user",
|
||
content,
|
||
agentId: null,
|
||
msgId,
|
||
attachments: localAttachments,
|
||
pending: hasAudio
|
||
// audio messages are pending until transcript arrives
|
||
});
|
||
const payload = { type: "message", content, msgId };
|
||
if (hasAttachments) payload.attachments = attachmentPayload;
|
||
wsSendFn(payload);
|
||
sending.value = false;
|
||
}
|
||
return {
|
||
sending,
|
||
input,
|
||
messagesEl,
|
||
parseMd,
|
||
scrollToBottom,
|
||
scrollIfAtBottom,
|
||
send,
|
||
onInputChange,
|
||
navigateHistory,
|
||
restoreLastSent,
|
||
startNewAssistantMessage: store.startNewAssistantMessage,
|
||
appendAssistantMessage: store.appendAssistantDelta,
|
||
finalizeAssistantMessage: store.finalizeAssistantMessage,
|
||
resetAssistantMessageState: store.resetLocalSession,
|
||
hasActiveStreamingMessage: store.hasActiveStreamingMessage,
|
||
streamingMessageVisibleContent: computed(() => store.streamingMessageVisibleContent)
|
||
};
|
||
}
|
||
function useMessageGrouping(messages, visibleCount, selectedAgent, allAgents, sessionKey) {
|
||
const VISIBLE_PAGE2 = 50;
|
||
const visibleMsgs = computed(() => {
|
||
const all = messages.value;
|
||
const start = Math.max(0, all.length - visibleCount.value);
|
||
return all.slice(start).map((m, i) => ({ ...m, _sourceIndex: start + i }));
|
||
});
|
||
const hasMore = computed(() => messages.value.length > visibleCount.value);
|
||
function loadMore() {
|
||
visibleCount.value += VISIBLE_PAGE2;
|
||
}
|
||
function getFormattedAgentName(agentId) {
|
||
if (!agentId) return "Unknown";
|
||
const agent = allAgents.value.find((a) => a.id === agentId);
|
||
return agent ? agent.name : agentId;
|
||
}
|
||
function shouldShowHeadline(index, msgsArr) {
|
||
if (index === 0) return true;
|
||
const current = msgsArr[index];
|
||
const prev = msgsArr[index - 1];
|
||
if (!current.agentId || !prev.agentId) {
|
||
return current.sessionId !== prev.sessionId;
|
||
}
|
||
return current.agentId !== prev.agentId || current.sessionId !== prev.sessionId;
|
||
}
|
||
function formatHeadlineText(agentName) {
|
||
const key = sessionKey?.value;
|
||
return key ? `${agentName} · ${key}` : agentName;
|
||
}
|
||
function getHeadline(index, msgsArr) {
|
||
const current = msgsArr[index];
|
||
const targetAgentId = current.agentId || selectedAgent.value;
|
||
const agentName = getFormattedAgentName(targetAgentId);
|
||
if (index === 0) return { text: formatHeadlineText(agentName), kind: "agent" };
|
||
const prev = msgsArr[index - 1];
|
||
if (current.agentId !== prev.agentId) return { text: formatHeadlineText(agentName), kind: "agent" };
|
||
if (current.sessionId !== prev.sessionId) return { text: "New Session", kind: "new-session" };
|
||
return { text: formatHeadlineText(agentName), kind: "agent" };
|
||
}
|
||
const groupedVisibleMsgs = computed(() => {
|
||
const raw = visibleMsgs.value;
|
||
const result = [];
|
||
let currentGroup = null;
|
||
for (let i = 0; i < raw.length; i++) {
|
||
const msg = raw[i];
|
||
if (shouldShowHeadline(i, raw)) {
|
||
if (currentGroup) {
|
||
result.push(currentGroup);
|
||
currentGroup = null;
|
||
}
|
||
const { text, kind } = getHeadline(i, raw);
|
||
result.push({
|
||
role: "system",
|
||
type: "headline",
|
||
content: text,
|
||
headlineKind: kind,
|
||
agentId: msg.agentId,
|
||
sessionId: msg.sessionId,
|
||
position: "header"
|
||
// Header appears before agent block
|
||
});
|
||
}
|
||
if (msg.role === "system" && msg.type !== "no_session") {
|
||
if (!currentGroup) {
|
||
currentGroup = { role: "system_group", messages: [msg], agentId: msg.agentId, sessionId: msg.sessionId };
|
||
} else {
|
||
currentGroup.messages.push(msg);
|
||
}
|
||
} else {
|
||
if (currentGroup) {
|
||
result.push(currentGroup);
|
||
currentGroup = null;
|
||
}
|
||
result.push(msg);
|
||
const effectiveAgentId = msg.agentId || selectedAgent.value;
|
||
if (effectiveAgentId && i === raw.length - 1) {
|
||
const agentName = getFormattedAgentName(effectiveAgentId);
|
||
result.push({
|
||
role: "system",
|
||
type: "headline",
|
||
content: formatHeadlineText(agentName),
|
||
headlineKind: "agent",
|
||
agentId: effectiveAgentId,
|
||
sessionId: msg.sessionId,
|
||
position: "footer"
|
||
});
|
||
}
|
||
}
|
||
}
|
||
if (currentGroup) result.push(currentGroup);
|
||
return result;
|
||
});
|
||
return {
|
||
visibleMsgs,
|
||
groupedVisibleMsgs,
|
||
hasMore,
|
||
loadMore,
|
||
getFormattedAgentName
|
||
};
|
||
}
|
||
function useInputAutogrow(input) {
|
||
const inputEl = ref(null);
|
||
const isShaking = ref(false);
|
||
function autoGrow() {
|
||
const el = inputEl.value;
|
||
if (!el) return;
|
||
el.style.height = "auto";
|
||
el.style.height = el.scrollHeight + "px";
|
||
el.style.overflowY = el.scrollHeight > 160 ? "auto" : "hidden";
|
||
}
|
||
function triggerShake() {
|
||
isShaking.value = true;
|
||
setTimeout(() => {
|
||
isShaking.value = false;
|
||
}, 400);
|
||
}
|
||
watch(input, (val) => {
|
||
if (!val) nextTick(() => autoGrow());
|
||
});
|
||
return { inputEl, isShaking, autoGrow, triggerShake };
|
||
}
|
||
function useAgentDisplay(selectedAgent, defaultAgent, allAgents) {
|
||
const chatStore = useChatStore();
|
||
const defaultAgentName = computed(() => {
|
||
const agent = allAgents.value.find((a) => a.id === defaultAgent.value);
|
||
return agent ? agent.name : defaultAgent.value;
|
||
});
|
||
const agentDisplayName = computed(() => {
|
||
const agent = allAgents.value.find((a) => a.id === selectedAgent.value);
|
||
return (agent ? agent.name : selectedAgent.value).toUpperCase();
|
||
});
|
||
const isAgentRunning = computed(() => chatStore.smState === "AGENT_RUNNING");
|
||
const agentStatusDone = computed(() => chatStore.channelState === "READY" || chatStore.channelState === "FRESH");
|
||
const agentStatus = computed(() => {
|
||
switch (chatStore.smState) {
|
||
case "CONNECTING":
|
||
return "⚙️ Connecting…";
|
||
case "AGENT_RUNNING":
|
||
return "⚙️ Working…";
|
||
case "HANDOVER_PENDING":
|
||
return "📝 Writing handover…";
|
||
case "HANDOVER_DONE":
|
||
return "✅ Handover ready";
|
||
case "SWITCHING":
|
||
return "🔀 Switching…";
|
||
default:
|
||
return null;
|
||
}
|
||
});
|
||
return { defaultAgentName, agentDisplayName, isAgentRunning, agentStatusDone, agentStatus };
|
||
}
|
||
const ACCEPTED_TYPES = [
|
||
"image/jpeg",
|
||
"image/png",
|
||
"image/gif",
|
||
"image/webp",
|
||
"application/pdf",
|
||
"audio/webm",
|
||
"audio/mp4",
|
||
"audio/ogg",
|
||
"audio/mpeg",
|
||
"audio/wav",
|
||
"audio/x-m4a"
|
||
];
|
||
const MAX_BYTES = 10 * 1024 * 1024;
|
||
function readAsBase64(file) {
|
||
return new Promise((resolve, reject) => {
|
||
const reader = new FileReader();
|
||
reader.onload = () => {
|
||
const result = reader.result;
|
||
const idx = result.indexOf(",");
|
||
resolve(idx >= 0 ? result.slice(idx + 1) : result);
|
||
};
|
||
reader.onerror = () => reject(reader.error);
|
||
reader.readAsDataURL(file);
|
||
});
|
||
}
|
||
function useAttachments() {
|
||
const attachments = ref([]);
|
||
async function addFiles(files) {
|
||
for (const file of Array.from(files)) {
|
||
const baseType = file.type.split(";")[0];
|
||
if (!ACCEPTED_TYPES.includes(baseType) && !ACCEPTED_TYPES.includes(file.type)) {
|
||
console.warn(`[attachments] skipped ${file.name}: unsupported type ${file.type}`);
|
||
continue;
|
||
}
|
||
if (file.size > MAX_BYTES) {
|
||
console.warn(`[attachments] skipped ${file.name}: exceeds 5MB (${(file.size / 1024 / 1024).toFixed(1)}MB)`);
|
||
continue;
|
||
}
|
||
const base64 = await readAsBase64(file);
|
||
const preview = URL.createObjectURL(file);
|
||
attachments.value.push({ file, preview, base64, mimeType: file.type, fileName: file.name });
|
||
}
|
||
}
|
||
function removeAttachment(index) {
|
||
const att = attachments.value[index];
|
||
if (att) URL.revokeObjectURL(att.preview);
|
||
attachments.value.splice(index, 1);
|
||
}
|
||
function clearAttachments() {
|
||
for (const att of attachments.value) URL.revokeObjectURL(att.preview);
|
||
attachments.value = [];
|
||
}
|
||
function toPayload() {
|
||
return attachments.value.map((a) => ({
|
||
type: a.mimeType.startsWith("image/") ? "image" : a.mimeType.startsWith("audio/") ? "audio" : "document",
|
||
mimeType: a.mimeType,
|
||
content: a.base64,
|
||
fileName: a.fileName
|
||
}));
|
||
}
|
||
function hasAttachments() {
|
||
return attachments.value.length > 0;
|
||
}
|
||
return { attachments, addFiles, removeAttachment, clearAttachments, toPayload, hasAttachments };
|
||
}
|
||
function useAudioRecorder() {
|
||
const isRecording = ref(false);
|
||
const duration = ref(0);
|
||
const audioLevel = ref(0);
|
||
const micDenied = ref(false);
|
||
let mediaRecorder = null;
|
||
let stream = null;
|
||
let audioCtx = null;
|
||
let analyser = null;
|
||
let levelBuf = null;
|
||
let chunks = [];
|
||
let timer = null;
|
||
let startTime = 0;
|
||
function cleanup() {
|
||
if (timer) {
|
||
clearInterval(timer);
|
||
timer = null;
|
||
}
|
||
if (audioCtx) {
|
||
audioCtx.close().catch(() => {
|
||
});
|
||
audioCtx = null;
|
||
analyser = null;
|
||
levelBuf = null;
|
||
}
|
||
if (stream) {
|
||
stream.getTracks().forEach((t) => t.stop());
|
||
stream = null;
|
||
}
|
||
mediaRecorder = null;
|
||
chunks = [];
|
||
duration.value = 0;
|
||
audioLevel.value = 0;
|
||
isRecording.value = false;
|
||
}
|
||
function updateLevel() {
|
||
if (!analyser || !levelBuf) return;
|
||
analyser.getByteTimeDomainData(levelBuf);
|
||
let sum = 0;
|
||
for (let i = 0; i < levelBuf.length; i++) {
|
||
const v = (levelBuf[i] - 128) / 128;
|
||
sum += v * v;
|
||
}
|
||
audioLevel.value = Math.min(1, Math.sqrt(sum / levelBuf.length) * 3);
|
||
}
|
||
async function startRecording() {
|
||
if (isRecording.value) return;
|
||
try {
|
||
stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||
} catch (err) {
|
||
console.warn("[audio] mic access denied:", err);
|
||
micDenied.value = true;
|
||
setTimeout(() => {
|
||
micDenied.value = false;
|
||
}, 5e3);
|
||
return;
|
||
}
|
||
try {
|
||
audioCtx = new AudioContext();
|
||
const source = audioCtx.createMediaStreamSource(stream);
|
||
analyser = audioCtx.createAnalyser();
|
||
analyser.fftSize = 256;
|
||
source.connect(analyser);
|
||
levelBuf = new Uint8Array(analyser.fftSize);
|
||
} catch (err) {
|
||
console.warn("[audio] analyser setup failed:", err);
|
||
}
|
||
chunks = [];
|
||
const mimeType = MediaRecorder.isTypeSupported("audio/webm;codecs=opus") ? "audio/webm;codecs=opus" : MediaRecorder.isTypeSupported("audio/webm") ? "audio/webm" : "";
|
||
mediaRecorder = new MediaRecorder(stream, mimeType ? { mimeType } : void 0);
|
||
mediaRecorder.ondataavailable = (e) => {
|
||
if (e.data.size > 0) chunks.push(e.data);
|
||
};
|
||
mediaRecorder.start(250);
|
||
isRecording.value = true;
|
||
startTime = Date.now();
|
||
timer = setInterval(() => {
|
||
duration.value = Math.floor((Date.now() - startTime) / 1e3);
|
||
updateLevel();
|
||
}, 80);
|
||
}
|
||
function stopRecording() {
|
||
return new Promise((resolve) => {
|
||
if (!mediaRecorder || mediaRecorder.state === "inactive") {
|
||
cleanup();
|
||
resolve(null);
|
||
return;
|
||
}
|
||
mediaRecorder.onstop = () => {
|
||
const mimeType = mediaRecorder?.mimeType || "audio/webm";
|
||
const ext = mimeType.includes("mp4") ? "mp4" : mimeType.includes("ogg") ? "ogg" : "webm";
|
||
const blob = new Blob(chunks, { type: mimeType });
|
||
const file = new File([blob], `recording-${Date.now()}.${ext}`, { type: mimeType });
|
||
cleanup();
|
||
resolve(file);
|
||
};
|
||
mediaRecorder.stop();
|
||
});
|
||
}
|
||
function cancelRecording() {
|
||
if (mediaRecorder && mediaRecorder.state !== "inactive") {
|
||
mediaRecorder.onstop = () => {
|
||
};
|
||
mediaRecorder.stop();
|
||
}
|
||
cleanup();
|
||
}
|
||
function formatDuration(secs) {
|
||
const m = Math.floor(secs / 60);
|
||
const s = secs % 60;
|
||
return `${m}:${s.toString().padStart(2, "0")}`;
|
||
}
|
||
onUnmounted(cleanup);
|
||
return { isRecording, duration, audioLevel, micDenied, startRecording, stopRecording, cancelRecording, formatDuration };
|
||
}
|
||
const _sfc_main$6 = /* @__PURE__ */ defineComponent({
|
||
__name: "MessageFrame",
|
||
__ssrInlineRender: true,
|
||
props: {
|
||
role: {},
|
||
copyContent: {}
|
||
},
|
||
setup(__props) {
|
||
return (_ctx, _push, _parent, _attrs) => {
|
||
_push(`<div${ssrRenderAttrs(mergeProps({
|
||
class: ["message", __props.role]
|
||
}, _attrs))}>`);
|
||
if (__props.role !== "system") {
|
||
_push(`<button class="copy-btn" title="Copy">⎘</button>`);
|
||
} else {
|
||
_push(`<!---->`);
|
||
}
|
||
ssrRenderSlot(_ctx.$slots, "default", {}, null, _push, _parent);
|
||
if (_ctx.$slots.footer) {
|
||
_push(`<div class="bubble-footer">`);
|
||
ssrRenderSlot(_ctx.$slots, "footer", {}, null, _push, _parent);
|
||
_push(`</div>`);
|
||
} else {
|
||
_push(`<!---->`);
|
||
}
|
||
_push(`</div>`);
|
||
};
|
||
}
|
||
});
|
||
const _sfc_setup$6 = _sfc_main$6.setup;
|
||
_sfc_main$6.setup = (props, ctx) => {
|
||
const ssrContext = useSSRContext();
|
||
(ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("src/components/MessageFrame.vue");
|
||
return _sfc_setup$6 ? _sfc_setup$6(props, ctx) : void 0;
|
||
};
|
||
const _sfc_main$5 = /* @__PURE__ */ defineComponent({
|
||
__name: "UserMessage",
|
||
__ssrInlineRender: true,
|
||
props: {
|
||
msg: {}
|
||
},
|
||
setup(__props) {
|
||
const props = __props;
|
||
const username = auth.currentUser;
|
||
const expandedSrc = ref("");
|
||
function expandImage(src) {
|
||
expandedSrc.value = src;
|
||
}
|
||
const audioAttachment = computed(() => (props.msg.attachments || []).find((a) => a.mimeType?.startsWith("audio/")));
|
||
const nonAudioAttachments = computed(() => (props.msg.attachments || []).filter((a) => !a.mimeType?.startsWith("audio/")));
|
||
return (_ctx, _push, _parent, _attrs) => {
|
||
_push(`<!--[-->`);
|
||
_push(ssrRenderComponent(_sfc_main$6, {
|
||
role: "user",
|
||
copyContent: __props.msg.content
|
||
}, createSlots({
|
||
default: withCtx((_, _push2, _parent2, _scopeId) => {
|
||
if (_push2) {
|
||
if (__props.msg.voiceAudioUrl) {
|
||
_push2(`<audio controls${ssrRenderAttr("src", __props.msg.voiceAudioUrl)} class="user-att-audio" data-v-90f9dace${_scopeId}></audio>`);
|
||
} else if (audioAttachment.value) {
|
||
_push2(`<audio controls${ssrRenderAttr("src", audioAttachment.value.dataUrl)} class="user-att-audio" data-v-90f9dace${_scopeId}></audio>`);
|
||
} else {
|
||
_push2(`<!---->`);
|
||
}
|
||
if (__props.msg.pending) {
|
||
_push2(`<div class="voice-pending" data-v-90f9dace${_scopeId}>transcribing...</div>`);
|
||
} else {
|
||
_push2(`<!---->`);
|
||
}
|
||
if (__props.msg.content) {
|
||
_push2(`<div data-v-90f9dace${_scopeId}>${ssrInterpolate(__props.msg.content)}</div>`);
|
||
} else {
|
||
_push2(`<!---->`);
|
||
}
|
||
if (nonAudioAttachments.value.length) {
|
||
_push2(`<div class="user-attachments" data-v-90f9dace${_scopeId}><!--[-->`);
|
||
ssrRenderList(nonAudioAttachments.value, (att, i) => {
|
||
_push2(`<!--[-->`);
|
||
if (att.mimeType === "application/pdf") {
|
||
_push2(`<div class="user-att-pdf"${ssrRenderAttr("title", att.fileName || "PDF")} data-v-90f9dace${_scopeId}><span class="pdf-icon" data-v-90f9dace${_scopeId}>📄</span><span class="pdf-name" data-v-90f9dace${_scopeId}>${ssrInterpolate(att.fileName || "document.pdf")}</span></div>`);
|
||
} else {
|
||
_push2(`<div class="user-att-thumb" data-v-90f9dace${_scopeId}><img${ssrRenderAttr("src", att.dataUrl)}${ssrRenderAttr("alt", att.fileName || "image")} loading="lazy" data-v-90f9dace${_scopeId}></div>`);
|
||
}
|
||
_push2(`<!--]-->`);
|
||
});
|
||
_push2(`<!--]--></div>`);
|
||
} else {
|
||
_push2(`<!---->`);
|
||
}
|
||
} else {
|
||
return [
|
||
__props.msg.voiceAudioUrl ? (openBlock(), createBlock("audio", {
|
||
key: 0,
|
||
controls: "",
|
||
src: __props.msg.voiceAudioUrl,
|
||
class: "user-att-audio"
|
||
}, null, 8, ["src"])) : audioAttachment.value ? (openBlock(), createBlock("audio", {
|
||
key: 1,
|
||
controls: "",
|
||
src: audioAttachment.value.dataUrl,
|
||
class: "user-att-audio"
|
||
}, null, 8, ["src"])) : createCommentVNode("", true),
|
||
__props.msg.pending ? (openBlock(), createBlock("div", {
|
||
key: 2,
|
||
class: "voice-pending"
|
||
}, "transcribing...")) : createCommentVNode("", true),
|
||
__props.msg.content ? (openBlock(), createBlock("div", { key: 3 }, toDisplayString(__props.msg.content), 1)) : createCommentVNode("", true),
|
||
nonAudioAttachments.value.length ? (openBlock(), createBlock("div", {
|
||
key: 4,
|
||
class: "user-attachments"
|
||
}, [
|
||
(openBlock(true), createBlock(Fragment, null, renderList(nonAudioAttachments.value, (att, i) => {
|
||
return openBlock(), createBlock(Fragment, { key: i }, [
|
||
att.mimeType === "application/pdf" ? (openBlock(), createBlock("div", {
|
||
key: 0,
|
||
class: "user-att-pdf",
|
||
title: att.fileName || "PDF"
|
||
}, [
|
||
createVNode("span", { class: "pdf-icon" }, "📄"),
|
||
createVNode("span", { class: "pdf-name" }, toDisplayString(att.fileName || "document.pdf"), 1)
|
||
], 8, ["title"])) : (openBlock(), createBlock("div", {
|
||
key: 1,
|
||
class: "user-att-thumb",
|
||
onClick: ($event) => expandImage(att.dataUrl)
|
||
}, [
|
||
createVNode("img", {
|
||
src: att.dataUrl,
|
||
alt: att.fileName || "image",
|
||
loading: "lazy"
|
||
}, null, 8, ["src", "alt"])
|
||
], 8, ["onClick"]))
|
||
], 64);
|
||
}), 128))
|
||
])) : createCommentVNode("", true)
|
||
];
|
||
}
|
||
}),
|
||
_: 2
|
||
}, [
|
||
unref(username) ? {
|
||
name: "footer",
|
||
fn: withCtx((_, _push2, _parent2, _scopeId) => {
|
||
if (_push2) {
|
||
_push2(`${ssrInterpolate(unref(username))}`);
|
||
} else {
|
||
return [
|
||
createTextVNode(toDisplayString(unref(username)), 1)
|
||
];
|
||
}
|
||
}),
|
||
key: "0"
|
||
} : void 0
|
||
]), _parent));
|
||
if (expandedSrc.value) {
|
||
_push(`<div class="lightbox-overlay" data-v-90f9dace><img${ssrRenderAttr("src", expandedSrc.value)} class="lightbox-img" data-v-90f9dace></div>`);
|
||
} else {
|
||
_push(`<!---->`);
|
||
}
|
||
_push(`<!--]-->`);
|
||
};
|
||
}
|
||
});
|
||
const _sfc_setup$5 = _sfc_main$5.setup;
|
||
_sfc_main$5.setup = (props, ctx) => {
|
||
const ssrContext = useSSRContext();
|
||
(ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("src/components/UserMessage.vue");
|
||
return _sfc_setup$5 ? _sfc_setup$5(props, ctx) : void 0;
|
||
};
|
||
const UserMessage = /* @__PURE__ */ _export_sfc(_sfc_main$5, [["__scopeId", "data-v-90f9dace"]]);
|
||
const _sfc_main$4 = /* @__PURE__ */ defineComponent({
|
||
__name: "ToolIcon",
|
||
__ssrInlineRender: true,
|
||
props: {
|
||
tool: {}
|
||
},
|
||
setup(__props) {
|
||
const props = __props;
|
||
const iconMap = {
|
||
read: BookOpenIcon,
|
||
write: PencilIcon,
|
||
edit: WrenchIcon,
|
||
append: DocumentPlusIcon,
|
||
exec: BoltIcon,
|
||
web_search: GlobeAltIcon,
|
||
web_fetch: GlobeAltIcon,
|
||
memory_search: CpuChipIcon,
|
||
memory_get: CpuChipIcon,
|
||
browser: ComputerDesktopIcon
|
||
};
|
||
const icon = computed(() => {
|
||
if (!props.tool) return BoltIcon;
|
||
const t = props.tool.toLowerCase();
|
||
if (iconMap[t]) return iconMap[t];
|
||
if (t.includes("message")) return ChatBubbleLeftIcon;
|
||
if (t.includes("session")) return LinkIcon;
|
||
return Cog6ToothIcon;
|
||
});
|
||
return (_ctx, _push, _parent, _attrs) => {
|
||
ssrRenderVNode(_push, createVNode(resolveDynamicComponent(icon.value), mergeProps({ class: "tool-icon w-3.5 h-3.5 inline shrink-0" }, _attrs), null), _parent);
|
||
};
|
||
}
|
||
});
|
||
const _sfc_setup$4 = _sfc_main$4.setup;
|
||
_sfc_main$4.setup = (props, ctx) => {
|
||
const ssrContext = useSSRContext();
|
||
(ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("src/components/ToolIcon.vue");
|
||
return _sfc_setup$4 ? _sfc_setup$4(props, ctx) : void 0;
|
||
};
|
||
function relativeTime(isoOrDate) {
|
||
const then = typeof isoOrDate === "string" ? new Date(isoOrDate) : isoOrDate;
|
||
const now = Date.now();
|
||
const diffMs = now - then.getTime();
|
||
if (diffMs < 0) return "just now";
|
||
const sec = Math.floor(diffMs / 1e3);
|
||
if (sec < 60) return "just now";
|
||
const min = Math.floor(sec / 60);
|
||
if (min < 60) return `${min}m ago`;
|
||
const hr = Math.floor(min / 60);
|
||
if (hr < 24) return `${hr}h ago`;
|
||
const days = Math.floor(hr / 24);
|
||
if (days < 30) return `${days}d ago`;
|
||
return then.toLocaleDateString();
|
||
}
|
||
const _sfc_main$3 = /* @__PURE__ */ defineComponent({
|
||
__name: "AssistantMessage",
|
||
__ssrInlineRender: true,
|
||
props: {
|
||
msg: {},
|
||
agentDisplayName: {},
|
||
isAgentRunning: { type: Boolean },
|
||
allAgents: {},
|
||
getToolsForTurn: { type: Function },
|
||
hudVersion: {}
|
||
},
|
||
setup(__props) {
|
||
const chatStore = useChatStore();
|
||
const ttsPlayer = useTtsPlayer();
|
||
const props = __props;
|
||
const displayName = computed(() => {
|
||
const id = props.msg.agentId;
|
||
if (id) {
|
||
const agent = props.allAgents?.find((a) => a.id === id);
|
||
return (agent ? agent.name : id).toUpperCase();
|
||
}
|
||
return props.agentDisplayName;
|
||
});
|
||
const msgTimeLabel = computed(() => {
|
||
if (props.msg.timestamp) {
|
||
return relativeTime(props.msg.timestamp);
|
||
}
|
||
return "Done";
|
||
});
|
||
const content = computed(() => {
|
||
if (props.msg.streaming) {
|
||
return chatStore.streamingMessageVisibleContent + '<span class="typing-dots">...</span>';
|
||
}
|
||
return props.msg.content;
|
||
});
|
||
const tools = computed(() => {
|
||
void props.hudVersion;
|
||
return props.getToolsForTurn(props.msg.turnCorrId);
|
||
});
|
||
return (_ctx, _push, _parent, _attrs) => {
|
||
_push(ssrRenderComponent(_sfc_main$6, mergeProps({
|
||
role: "assistant",
|
||
copyContent: __props.msg.content
|
||
}, _attrs), {
|
||
footer: withCtx((_, _push2, _parent2, _scopeId) => {
|
||
if (_push2) {
|
||
_push2(`<span class="footer-name" data-v-a59db10d${_scopeId}>${ssrInterpolate(displayName.value)}</span>`);
|
||
if (!__props.msg.streaming && __props.msg.content) {
|
||
_push2(`<button class="${ssrRenderClass([{ active: unref(ttsPlayer).isPlayingMsg(__props.msg) }, "tts-btn"])}" title="Listen" data-v-a59db10d${_scopeId}>`);
|
||
if (unref(ttsPlayer).isPlayingMsg(__props.msg) && unref(ttsPlayer).state.value === "loading") {
|
||
_push2(`<span class="tts-spinner" data-v-a59db10d${_scopeId}></span>`);
|
||
} else {
|
||
_push2(ssrRenderComponent(unref(SpeakerWaveIcon), { class: "w-3.5 h-3.5" }, null, _parent2, _scopeId));
|
||
}
|
||
_push2(`</button>`);
|
||
} else {
|
||
_push2(`<!---->`);
|
||
}
|
||
if (__props.msg.streaming && tools.value.length === 0) {
|
||
_push2(`<span class="footer-status" data-v-a59db10d${_scopeId}> ...</span>`);
|
||
} else if (!__props.msg.streaming && tools.value.length === 0) {
|
||
_push2(`<span class="footer-status" data-v-a59db10d${_scopeId}> | ${ssrInterpolate(msgTimeLabel.value)}</span>`);
|
||
} else {
|
||
_push2(`<!---->`);
|
||
}
|
||
if (tools.value.length > 0) {
|
||
_push2(`<span class="footer-tools" data-v-a59db10d${_scopeId}><!--[-->`);
|
||
ssrRenderList(tools.value, (tool) => {
|
||
_push2(`<span class="${ssrRenderClass([tool.state, "footer-tool-icon"])}"${ssrRenderAttr("title", `${tool.label} [${tool.state}]`)} data-v-a59db10d${_scopeId}>`);
|
||
_push2(ssrRenderComponent(_sfc_main$4, {
|
||
tool: tool.tool || ""
|
||
}, null, _parent2, _scopeId));
|
||
_push2(`</span>`);
|
||
});
|
||
_push2(`<!--]--></span>`);
|
||
} else {
|
||
_push2(`<!---->`);
|
||
}
|
||
if (__props.msg.truncated) {
|
||
_push2(`<span class="truncated-notice" data-v-a59db10d${_scopeId}>`);
|
||
_push2(ssrRenderComponent(unref(ExclamationTriangleIcon), { class: "w-4 h-4 inline" }, null, _parent2, _scopeId));
|
||
_push2(` Output limit reached — response was cut off</span>`);
|
||
} else {
|
||
_push2(`<!---->`);
|
||
}
|
||
} else {
|
||
return [
|
||
createVNode("span", { class: "footer-name" }, toDisplayString(displayName.value), 1),
|
||
!__props.msg.streaming && __props.msg.content ? (openBlock(), createBlock("button", {
|
||
key: 0,
|
||
class: ["tts-btn", { active: unref(ttsPlayer).isPlayingMsg(__props.msg) }],
|
||
onClick: withModifiers(($event) => unref(ttsPlayer).play(__props.msg, __props.msg._sourceIndex ?? 0), ["stop"]),
|
||
title: "Listen"
|
||
}, [
|
||
unref(ttsPlayer).isPlayingMsg(__props.msg) && unref(ttsPlayer).state.value === "loading" ? (openBlock(), createBlock("span", {
|
||
key: 0,
|
||
class: "tts-spinner"
|
||
})) : (openBlock(), createBlock(unref(SpeakerWaveIcon), {
|
||
key: 1,
|
||
class: "w-3.5 h-3.5"
|
||
}))
|
||
], 10, ["onClick"])) : createCommentVNode("", true),
|
||
__props.msg.streaming && tools.value.length === 0 ? (openBlock(), createBlock("span", {
|
||
key: 1,
|
||
class: "footer-status"
|
||
}, " ...")) : !__props.msg.streaming && tools.value.length === 0 ? (openBlock(), createBlock("span", {
|
||
key: 2,
|
||
class: "footer-status"
|
||
}, " | " + toDisplayString(msgTimeLabel.value), 1)) : createCommentVNode("", true),
|
||
tools.value.length > 0 ? (openBlock(), createBlock("span", {
|
||
key: 3,
|
||
class: "footer-tools"
|
||
}, [
|
||
(openBlock(true), createBlock(Fragment, null, renderList(tools.value, (tool) => {
|
||
return openBlock(), createBlock("span", {
|
||
key: tool.id,
|
||
class: ["footer-tool-icon", tool.state],
|
||
title: `${tool.label} [${tool.state}]`
|
||
}, [
|
||
createVNode(_sfc_main$4, {
|
||
tool: tool.tool || ""
|
||
}, null, 8, ["tool"])
|
||
], 10, ["title"]);
|
||
}), 128))
|
||
])) : createCommentVNode("", true),
|
||
__props.msg.truncated ? (openBlock(), createBlock("span", {
|
||
key: 4,
|
||
class: "truncated-notice"
|
||
}, [
|
||
createVNode(unref(ExclamationTriangleIcon), { class: "w-4 h-4 inline" }),
|
||
createTextVNode(" Output limit reached — response was cut off")
|
||
])) : createCommentVNode("", true)
|
||
];
|
||
}
|
||
}),
|
||
default: withCtx((_, _push2, _parent2, _scopeId) => {
|
||
if (_push2) {
|
||
_push2(`<div data-v-a59db10d${_scopeId}>${unref(parseMd)(content.value) ?? ""}</div>`);
|
||
} else {
|
||
return [
|
||
createVNode("div", {
|
||
innerHTML: unref(parseMd)(content.value)
|
||
}, null, 8, ["innerHTML"])
|
||
];
|
||
}
|
||
}),
|
||
_: 1
|
||
}, _parent));
|
||
};
|
||
}
|
||
});
|
||
const _sfc_setup$3 = _sfc_main$3.setup;
|
||
_sfc_main$3.setup = (props, ctx) => {
|
||
const ssrContext = useSSRContext();
|
||
(ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("src/components/AssistantMessage.vue");
|
||
return _sfc_setup$3 ? _sfc_setup$3(props, ctx) : void 0;
|
||
};
|
||
const AssistantMessage = /* @__PURE__ */ _export_sfc(_sfc_main$3, [["__scopeId", "data-v-a59db10d"]]);
|
||
const _sfc_main$2 = /* @__PURE__ */ defineComponent({
|
||
__name: "SystemMessage",
|
||
__ssrInlineRender: true,
|
||
props: {
|
||
msg: {}
|
||
},
|
||
setup(__props) {
|
||
const props = __props;
|
||
const isCollapsed = ref(false);
|
||
const groupContentEl = ref(null);
|
||
onUpdated(() => {
|
||
nextTick(() => {
|
||
if (groupContentEl.value) {
|
||
groupContentEl.value.scrollTop = groupContentEl.value.scrollHeight;
|
||
}
|
||
});
|
||
});
|
||
function isSqlResult(content) {
|
||
if (!content) return false;
|
||
const raw = content.startsWith("→ ") ? content.slice(2) : content;
|
||
const lines = raw.split("\n").filter((l) => l.trim());
|
||
if (lines.length < 2) return false;
|
||
return lines.filter((l) => l.includes(" ")).length >= 2;
|
||
}
|
||
function parseSqlTable(content) {
|
||
const raw = content.startsWith("→ ") ? content.slice(2) : content;
|
||
const lines = raw.split("\n").filter((l) => l.trim());
|
||
const [headerLine, ...dataLines] = lines;
|
||
const headers = headerLine.split(" ");
|
||
const rows = dataLines.filter((l) => !l.startsWith("… [")).map((l) => l.split(" "));
|
||
return { headers, rows };
|
||
}
|
||
function getSummary() {
|
||
const msgs = props.msg.messages;
|
||
if (!msgs?.length) return "Event";
|
||
const toolNames = [];
|
||
for (const m of msgs) {
|
||
const raw = m.content || "";
|
||
const match = raw.match(/^[^\w]*(\w+)/u);
|
||
if (match) {
|
||
const name = match[1];
|
||
if (!["true", "false", "null", "done", "ok"].includes(name.toLowerCase())) {
|
||
if (!toolNames.includes(name)) toolNames.push(name);
|
||
}
|
||
}
|
||
}
|
||
const callCount = msgs.length;
|
||
const label = toolNames.length ? toolNames.join(" · ") : "Event";
|
||
return callCount === 1 ? label : `${label} · ${callCount}`;
|
||
}
|
||
return (_ctx, _push, _parent, _attrs) => {
|
||
_push(`<div${ssrRenderAttrs(mergeProps({ class: "message-hud" }, _attrs))} data-v-6cdcd5f1>`);
|
||
if (__props.msg.type === "headline" && __props.msg.headlineKind !== "new-session" && __props.msg.position !== "footer") {
|
||
_push(`<div class="headline-container headline-header" data-v-6cdcd5f1><div class="headline-line" data-v-6cdcd5f1></div><div class="headline-text" data-v-6cdcd5f1>${ssrInterpolate(__props.msg.content)}</div><div class="headline-line" data-v-6cdcd5f1></div></div>`);
|
||
} else if (__props.msg.type === "headline" && __props.msg.headlineKind !== "new-session" && __props.msg.position === "footer") {
|
||
_push(`<div class="headline-footer-wrapper" data-v-6cdcd5f1><div class="headline-container headline-footer" data-v-6cdcd5f1><div class="headline-line" data-v-6cdcd5f1></div><div class="headline-text" data-v-6cdcd5f1>${ssrInterpolate(__props.msg.content)}</div><div class="headline-line" data-v-6cdcd5f1></div></div></div>`);
|
||
} else if (__props.msg.type === "headline" && __props.msg.headlineKind === "new-session") {
|
||
_push(`<div class="headline-new-session" data-v-6cdcd5f1><span class="new-session-text" data-v-6cdcd5f1>${ssrInterpolate(__props.msg.content)}</span></div>`);
|
||
} else if (__props.msg.role === "system_group" || __props.msg.messages) {
|
||
_push(`<div class="system-group" data-v-6cdcd5f1><div class="system-group-header" data-v-6cdcd5f1>`);
|
||
_push(ssrRenderComponent(unref(Cog6ToothIcon), { class: "system-group-icon-svg" }, null, _parent));
|
||
_push(`<span class="system-group-summary" data-v-6cdcd5f1>${ssrInterpolate(getSummary())}</span>`);
|
||
_push(ssrRenderComponent(unref(ChevronDownIcon), {
|
||
class: ["chevron", { open: !isCollapsed.value }]
|
||
}, null, _parent));
|
||
_push(`</div>`);
|
||
if (!isCollapsed.value) {
|
||
_push(`<div class="system-group-content" data-v-6cdcd5f1><!--[-->`);
|
||
ssrRenderList(__props.msg.messages, (subMsg, idx) => {
|
||
_push(`<div class="system-item" data-v-6cdcd5f1>`);
|
||
if (isSqlResult(subMsg.content)) {
|
||
_push(`<div class="sql-table-wrap" data-v-6cdcd5f1><table class="sql-table" data-v-6cdcd5f1><thead data-v-6cdcd5f1><tr data-v-6cdcd5f1><!--[-->`);
|
||
ssrRenderList(parseSqlTable(subMsg.content).headers, (col, ci) => {
|
||
_push(`<th data-v-6cdcd5f1>${ssrInterpolate(col)}</th>`);
|
||
});
|
||
_push(`<!--]--></tr></thead><tbody data-v-6cdcd5f1><!--[-->`);
|
||
ssrRenderList(parseSqlTable(subMsg.content).rows, (row, ri) => {
|
||
_push(`<tr data-v-6cdcd5f1><!--[-->`);
|
||
ssrRenderList(row, (cell, ci) => {
|
||
_push(`<td data-v-6cdcd5f1>${ssrInterpolate(cell)}</td>`);
|
||
});
|
||
_push(`<!--]--></tr>`);
|
||
});
|
||
_push(`<!--]--></tbody></table></div>`);
|
||
} else {
|
||
_push(`<div class="system-content raw-text"${ssrRenderAttr("title", subMsg.content)} data-v-6cdcd5f1>${ssrInterpolate(subMsg.content)}</div>`);
|
||
}
|
||
_push(`</div>`);
|
||
});
|
||
_push(`<!--]--></div>`);
|
||
} else {
|
||
_push(`<!---->`);
|
||
}
|
||
_push(`</div>`);
|
||
} else {
|
||
_push(`<div class="system-group" data-v-6cdcd5f1><div class="system-group-header" data-v-6cdcd5f1>`);
|
||
_push(ssrRenderComponent(unref(InformationCircleIcon), { class: "system-group-icon-svg" }, null, _parent));
|
||
_push(`<span class="system-group-summary" data-v-6cdcd5f1>${ssrInterpolate(__props.msg.content)}</span></div></div>`);
|
||
}
|
||
_push(`</div>`);
|
||
};
|
||
}
|
||
});
|
||
const _sfc_setup$2 = _sfc_main$2.setup;
|
||
_sfc_main$2.setup = (props, ctx) => {
|
||
const ssrContext = useSSRContext();
|
||
(ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("src/components/SystemMessage.vue");
|
||
return _sfc_setup$2 ? _sfc_setup$2(props, ctx) : void 0;
|
||
};
|
||
const SystemMessage = /* @__PURE__ */ _export_sfc(_sfc_main$2, [["__scopeId", "data-v-6cdcd5f1"]]);
|
||
const _sfc_main$1 = /* @__PURE__ */ defineComponent({
|
||
__name: "HudControls",
|
||
__ssrInlineRender: true,
|
||
props: {
|
||
smState: String,
|
||
connected: Boolean,
|
||
isAgentRunning: Boolean,
|
||
handoverPending: Boolean,
|
||
isPublic: Boolean
|
||
},
|
||
emits: ["new", "handover", "confirm-new", "stay"],
|
||
setup(__props) {
|
||
return (_ctx, _push, _parent, _attrs) => {
|
||
_push(`<div${ssrRenderAttrs(mergeProps({ class: "hud-controls" }, _attrs))} data-v-89c5a82c><div class="btn-group primary" data-v-89c5a82c><button class="control-btn"${ssrIncludeBooleanAttr(!__props.connected || __props.isAgentRunning || __props.handoverPending || __props.smState === "NO_SESSION" || __props.smState === "RESETTING") ? " disabled" : ""} data-v-89c5a82c>NEW</button>`);
|
||
if (!__props.isPublic) {
|
||
_push(`<button class="control-btn"${ssrIncludeBooleanAttr(!__props.connected || __props.isAgentRunning || __props.handoverPending || __props.smState === "NO_SESSION" || __props.smState === "RESETTING") ? " disabled" : ""} data-v-89c5a82c>HANDOVER</button>`);
|
||
} else {
|
||
_push(`<!---->`);
|
||
}
|
||
_push(`</div><div class="btn-group secondary" data-v-89c5a82c><button class="control-btn confirm-btn"${ssrIncludeBooleanAttr(!__props.handoverPending) ? " disabled" : ""} data-v-89c5a82c>YES, NEW</button><button class="control-btn stay-btn"${ssrIncludeBooleanAttr(!__props.handoverPending) ? " disabled" : ""} data-v-89c5a82c>STAY</button><button class="control-btn ns-btn"${ssrIncludeBooleanAttr(__props.smState !== "NO_SESSION") ? " disabled" : ""} data-v-89c5a82c>NEW SESSION</button></div></div>`);
|
||
};
|
||
}
|
||
});
|
||
const _sfc_setup$1 = _sfc_main$1.setup;
|
||
_sfc_main$1.setup = (props, ctx) => {
|
||
const ssrContext = useSSRContext();
|
||
(ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("src/components/HudControls.vue");
|
||
return _sfc_setup$1 ? _sfc_setup$1(props, ctx) : void 0;
|
||
};
|
||
const HudControls = /* @__PURE__ */ _export_sfc(_sfc_main$1, [["__scopeId", "data-v-89c5a82c"]]);
|
||
const VISIBLE_PAGE = 50;
|
||
const _sfc_main = /* @__PURE__ */ defineComponent({
|
||
...{ name: "AgentsView" },
|
||
__name: "AgentsView",
|
||
__ssrInlineRender: true,
|
||
setup(__props) {
|
||
useRouter();
|
||
const agentsRoute = useRoute();
|
||
const chatStore = useChatStore();
|
||
const showPicker = computed(() => !agentsRoute.query.agent);
|
||
watch(() => agentsRoute.query.agent, (val) => {
|
||
if (!val && agentsRoute.name === "agents") ;
|
||
}, { immediate: true });
|
||
const prevSessions = ref([]);
|
||
const prevHasMore = ref(false);
|
||
const prevLoading = ref(false);
|
||
const prevSkip = ref(0);
|
||
const prevMessages = computed(() => prevSessions.value.flatMap((s) => s.messages));
|
||
async function fetchPreviousSession(loadMore2 = false) {
|
||
if (!selectedAgent.value || prevLoading.value) return;
|
||
prevLoading.value = true;
|
||
try {
|
||
const token = localStorage.getItem("nyx_session") || localStorage.getItem("titan_token") || "";
|
||
const skip = loadMore2 ? prevSkip.value : 0;
|
||
const apiBase = getApiBase();
|
||
const res = await fetch(`${apiBase}/api/session-history?agent=${selectedAgent.value}&mode=${selectedMode.value}&skip=${skip}&count=1`, {
|
||
headers: { "Authorization": `Bearer ${token}` }
|
||
});
|
||
if (!res.ok) return;
|
||
const data = await res.json();
|
||
const newSessions = (data.sessions || []).map((s) => ({
|
||
messages: (s.entries || []).map((e) => ({
|
||
role: e.role,
|
||
content: e.content,
|
||
timestamp: e.timestamp,
|
||
agentId: selectedAgent.value,
|
||
streaming: false
|
||
})),
|
||
timestamp: s.resetTimestamp,
|
||
timeLabel: s.resetTimestamp ? relativeTime(s.resetTimestamp) : ""
|
||
}));
|
||
if (loadMore2) {
|
||
prevSessions.value = [...newSessions, ...prevSessions.value];
|
||
} else {
|
||
prevSessions.value = newSessions;
|
||
}
|
||
prevSkip.value = skip + newSessions.length;
|
||
prevHasMore.value = data.hasMore ?? false;
|
||
} catch {
|
||
} finally {
|
||
prevLoading.value = false;
|
||
}
|
||
}
|
||
const { connected, status, send: wsSend } = ws;
|
||
useUI(status);
|
||
ref("");
|
||
const { isLoggedIn } = auth;
|
||
const { selectedAgent, selectedMode, filteredAgents, defaultAgent, allAgents } = agents;
|
||
const SEGMENTS = ["personal", "common", "private", "public"];
|
||
const agentSegments = computed(
|
||
() => SEGMENTS.map((key) => ({
|
||
key,
|
||
agents: filteredAgents.value.filter((a) => (a.segment ?? "utility") === key).sort((a, b) => a.name.localeCompare(b.name))
|
||
})).filter((s) => s.agents.length > 0)
|
||
);
|
||
computed(() => agentLogo(selectedAgent.value));
|
||
const { sending, input, messagesEl, scrollToBottom, restoreLastSent } = useMessages(wsSend);
|
||
const controlsEl = ref(null);
|
||
ref(null);
|
||
const smState = toRef(chatStore, "smState");
|
||
const channelState = toRef(chatStore, "channelState");
|
||
const connectionState = toRef(chatStore, "connectionState");
|
||
const smLabel = toRef(chatStore, "smLabel");
|
||
const isHidden = computed(() => connectionState.value !== "SYNCED");
|
||
const footerHeadline = computed(() => {
|
||
const msgs = groupedVisibleMsgs.value;
|
||
const last = msgs[msgs.length - 1];
|
||
return last?.position === "footer" ? last : null;
|
||
});
|
||
const lastRealMsgIdx = computed(() => {
|
||
const msgs = groupedVisibleMsgs.value;
|
||
for (let i = msgs.length - 1; i >= 0; i--) {
|
||
if (msgs[i].type !== "headline") return i;
|
||
}
|
||
return -1;
|
||
});
|
||
const visibleCount = ref(VISIBLE_PAGE);
|
||
const { groupedVisibleMsgs, hasMore, loadMore, getFormattedAgentName } = useMessageGrouping(
|
||
computed(() => chatStore.messages),
|
||
visibleCount,
|
||
selectedAgent,
|
||
allAgents,
|
||
toRef(chatStore, "sessionKey")
|
||
);
|
||
const sentMessages = /* @__PURE__ */ new Set();
|
||
const lastUsage = ref(null);
|
||
const pendingClearRef = ref(false);
|
||
const { mount, hudVersion, getToolsForTurn, hudSnapshot, toolCallMapSnapshot } = useAgentSocket(visibleCount, lastUsage, pendingClearRef, sentMessages, restoreLastSent);
|
||
if (typeof window !== "undefined") {
|
||
window.__hudSnapshot = hudSnapshot;
|
||
window.__toolCallMap = toolCallMapSnapshot;
|
||
}
|
||
const viewActive = computed(() => agentsRoute.name === "agents");
|
||
let userSendInFlight = false;
|
||
const spacerHeight = ref(0);
|
||
function getViewport() {
|
||
const el = messagesEl.value;
|
||
if (!el) return null;
|
||
const root = el.$el || el;
|
||
return root.querySelector("[data-overlayscrollbars-viewport]");
|
||
}
|
||
function recalcSpacerSync() {
|
||
const viewport = getViewport();
|
||
if (!viewport) return 0;
|
||
const controls = controlsEl.value || viewport.querySelector(".msg-controls");
|
||
if (!controls) return 0;
|
||
const userMsgs = viewport.querySelectorAll(".message.user");
|
||
const last = userMsgs[userMsgs.length - 1];
|
||
if (!last) return 0;
|
||
const vpRect = viewport.getBoundingClientRect();
|
||
const st = viewport.scrollTop;
|
||
const msgTop = last.getBoundingClientRect().top - vpRect.top + st;
|
||
const ctrlBottom = controls.getBoundingClientRect().bottom - vpRect.top + st;
|
||
const vpH = viewport.clientHeight;
|
||
const needed = msgTop - 40 + vpH;
|
||
const h = Math.max(0, needed - ctrlBottom);
|
||
spacerHeight.value = h;
|
||
return h;
|
||
}
|
||
let _spacerInterval = null;
|
||
onMounted(() => {
|
||
_spacerInterval = setInterval(recalcSpacerSync, 200);
|
||
onUnmounted(() => {
|
||
if (_spacerInterval) clearInterval(_spacerInterval);
|
||
});
|
||
});
|
||
watch(connectionState, (val) => {
|
||
if (!viewActive.value) return;
|
||
if (val === "SYNCED") {
|
||
if (!prevMessages.value.length) fetchPreviousSession();
|
||
scrollToBottom();
|
||
}
|
||
});
|
||
watch(channelState, (val, prev) => {
|
||
if (!viewActive.value) return;
|
||
if (prev === "AGENT_RUNNING" && (val === "READY" || val === "FRESH")) {
|
||
userSendInFlight = false;
|
||
}
|
||
if (val === "AGENT_RUNNING" && prev !== "AGENT_RUNNING") {
|
||
if (!userSendInFlight) {
|
||
scrollToBottom();
|
||
}
|
||
}
|
||
});
|
||
function onNew() {
|
||
chatStore.newSession();
|
||
scrollToBottom();
|
||
}
|
||
function onConfirmNew() {
|
||
chatStore.confirmNew();
|
||
scrollToBottom();
|
||
}
|
||
const { isShaking } = useInputAutogrow(input);
|
||
const { isAgentRunning } = useAgentDisplay(selectedAgent, defaultAgent, allAgents);
|
||
const { attachments, hasAttachments } = useAttachments();
|
||
ref(null);
|
||
const isDragOver = ref(false);
|
||
const { isRecording, duration, audioLevel, micDenied, formatDuration } = useAudioRecorder();
|
||
watch([selectedAgent, selectedMode], ([agent, mode], [oldAgent, oldMode]) => {
|
||
if (agent !== oldAgent || mode !== oldMode) {
|
||
sessionStorage.setItem("agent", agent);
|
||
lastUsage.value = null;
|
||
sentMessages.clear();
|
||
prevSessions.value = [];
|
||
prevHasMore.value = false;
|
||
prevSkip.value = 0;
|
||
chatStore.resetLocalSession();
|
||
}
|
||
});
|
||
onMounted(() => {
|
||
chatStore.setWsSend(wsSend);
|
||
const unsubscribe = mount();
|
||
if (connectionState.value === "SYNCED") fetchPreviousSession();
|
||
const viewport = getViewport();
|
||
let ro = null;
|
||
if (viewport) {
|
||
ro = new ResizeObserver(() => recalcSpacerSync());
|
||
ro.observe(viewport);
|
||
}
|
||
recalcSpacerSync();
|
||
onUnmounted(() => {
|
||
unsubscribe();
|
||
ro?.disconnect();
|
||
});
|
||
});
|
||
return (_ctx, _push, _parent, _attrs) => {
|
||
const _component_RouterLink = resolveComponent("RouterLink");
|
||
if (unref(isLoggedIn)) {
|
||
_push(`<div${ssrRenderAttrs(mergeProps({ class: "agents-view flex flex-col h-full overflow-hidden bg-bg" }, _attrs))} data-v-8c19bd9d><div class="agent-picker" style="${ssrRenderStyle(showPicker.value ? null : { display: "none" })}" data-v-8c19bd9d><div class="agent-picker-content" data-v-8c19bd9d><p class="agent-picker-title" data-v-8c19bd9d>Select an agent</p><!--[-->`);
|
||
ssrRenderList(agentSegments.value, (seg) => {
|
||
_push(`<div class="agent-picker-segment" data-v-8c19bd9d><div class="agent-picker-segment-label" data-v-8c19bd9d>${ssrInterpolate(seg.key)}</div><!--[-->`);
|
||
ssrRenderList(seg.agents, (agent) => {
|
||
_push(`<div class="agent-picker-row" data-v-8c19bd9d><button class="agent-picker-btn"${ssrRenderAttr("title", agent.name + " (private)")} data-v-8c19bd9d><span class="${ssrRenderClass([`dot-${agent.role}`, "sidebar-room-dot"])}" data-v-8c19bd9d></span><span class="agent-picker-name" data-v-8c19bd9d>${ssrInterpolate(agent.name)}</span></button><div class="agent-picker-modes" data-v-8c19bd9d>`);
|
||
if (agent.modes?.includes("private")) {
|
||
_push(`<button class="agent-picker-mode" title="Private" data-v-8c19bd9d>`);
|
||
_push(ssrRenderComponent(unref(LockClosedIcon), { class: "w-3.5 h-3.5" }, null, _parent));
|
||
_push(`</button>`);
|
||
} else {
|
||
_push(`<!---->`);
|
||
}
|
||
if (agent.modes?.includes("public")) {
|
||
_push(`<button class="agent-picker-mode" title="Public" data-v-8c19bd9d>`);
|
||
_push(ssrRenderComponent(unref(UserGroupIcon), { class: "w-3.5 h-3.5" }, null, _parent));
|
||
_push(`</button>`);
|
||
} else {
|
||
_push(`<!---->`);
|
||
}
|
||
_push(`</div></div>`);
|
||
});
|
||
_push(`<!--]--></div>`);
|
||
});
|
||
_push(`<!--]--></div></div><div class="page flex flex-col flex-1 min-h-0" style="${ssrRenderStyle(!showPicker.value ? null : { display: "none" })}" data-v-8c19bd9d><div class="chat-frame flex-1 flex flex-col overflow-visible relative" data-v-8c19bd9d><div class="content flex-1 min-h-0" data-v-8c19bd9d>`);
|
||
_push(ssrRenderComponent(unref(OverlayScrollbarsComponent), {
|
||
class: ["messages h-full pb-4 flex flex-col gap-3 relative", { "is-switching": isHidden.value }],
|
||
options: unref(scrollbarOptions),
|
||
ref_key: "messagesEl",
|
||
ref: messagesEl,
|
||
element: "div"
|
||
}, {
|
||
default: withCtx((_, _push2, _parent2, _scopeId) => {
|
||
if (_push2) {
|
||
if (prevSessions.value.length) {
|
||
_push2(`<!--[-->`);
|
||
if (prevHasMore.value) {
|
||
_push2(`<button class="prev-load-more"${ssrIncludeBooleanAttr(prevLoading.value) ? " disabled" : ""} data-v-8c19bd9d${_scopeId}>${ssrInterpolate(prevLoading.value ? "Loading..." : "Load older session")}</button>`);
|
||
} else {
|
||
_push2(`<!---->`);
|
||
}
|
||
_push2(`<!--[-->`);
|
||
ssrRenderList(prevSessions.value, (session, si) => {
|
||
_push2(`<!--[--><div class="prev-session-header" data-v-8c19bd9d${_scopeId}> Session ${ssrInterpolate(prevSessions.value.length - si)}${ssrInterpolate(session.timeLabel ? " — " + session.timeLabel : "")}</div><div class="prev-session-wrapper" data-v-8c19bd9d${_scopeId}><!--[-->`);
|
||
ssrRenderList(session.messages, (msg, mi) => {
|
||
_push2(`<!--[-->`);
|
||
if (msg.role === "user") {
|
||
_push2(ssrRenderComponent(UserMessage, { msg }, null, _parent2, _scopeId));
|
||
} else if (msg.role === "assistant") {
|
||
_push2(ssrRenderComponent(AssistantMessage, {
|
||
msg,
|
||
agentDisplayName: unref(getFormattedAgentName)(msg.agentId || unref(selectedAgent)),
|
||
isAgentRunning: false,
|
||
allAgents: unref(allAgents),
|
||
getToolsForTurn: () => [],
|
||
hudVersion: 0
|
||
}, null, _parent2, _scopeId));
|
||
} else {
|
||
_push2(`<!---->`);
|
||
}
|
||
_push2(`<!--]-->`);
|
||
});
|
||
_push2(`<!--]--></div><!--]-->`);
|
||
});
|
||
_push2(`<!--]--><div class="session-divider" data-v-8c19bd9d${_scopeId}><span class="session-divider-text" data-v-8c19bd9d${_scopeId}>Current session</span></div><!--]-->`);
|
||
} else {
|
||
_push2(`<!---->`);
|
||
}
|
||
_push2(`<!--[-->`);
|
||
ssrRenderList(unref(groupedVisibleMsgs), (msg, i) => {
|
||
_push2(`<!--[-->`);
|
||
if (msg.type === "no_session") {
|
||
_push2(`<div class="no-session-center" data-v-8c19bd9d${_scopeId}>— NO SESSION —</div>`);
|
||
} else {
|
||
_push2(`<!---->`);
|
||
}
|
||
if (msg.type !== "no_session" && (msg.role === "system" || msg.role === "system_group" || msg.type === "headline") && msg.position !== "footer") {
|
||
_push2(ssrRenderComponent(SystemMessage, { msg }, null, _parent2, _scopeId));
|
||
} else {
|
||
_push2(`<!---->`);
|
||
}
|
||
if (unref(hasMore) && msg.type === "headline" && msg.position !== "footer" && i === 0) {
|
||
_push2(`<button class="load-more-btn" data-v-8c19bd9d${_scopeId}>↑ Load previous messages</button>`);
|
||
} else if (msg.role === "session_context") {
|
||
_push2(`<div class="session-context-badge" data-v-8c19bd9d${_scopeId}><span class="session-context-text" data-v-8c19bd9d${_scopeId}>${ssrInterpolate(msg.content)}</span></div>`);
|
||
} else if (msg.role === "user") {
|
||
_push2(ssrRenderComponent(UserMessage, { msg }, null, _parent2, _scopeId));
|
||
} else if (msg.role === "thinking") {
|
||
_push2(`<div class="message thinking" data-v-8c19bd9d${_scopeId}><details${ssrIncludeBooleanAttr(!msg.collapsed) ? " open" : ""} data-v-8c19bd9d${_scopeId}><summary data-v-8c19bd9d${_scopeId}>`);
|
||
_push2(ssrRenderComponent(unref(ChatBubbleBottomCenterTextIcon), { class: "w-4 h-4 inline" }, null, _parent2, _scopeId));
|
||
_push2(` thinking…</summary><pre class="thinking-content" data-v-8c19bd9d${_scopeId}>${ssrInterpolate(typeof msg.content === "object" ? msg.content.value : msg.content)}</pre></details></div>`);
|
||
} else if (msg.role === "assistant") {
|
||
_push2(ssrRenderComponent(AssistantMessage, {
|
||
msg,
|
||
agentDisplayName: unref(getFormattedAgentName)(msg.agentId),
|
||
isAgentRunning: unref(isAgentRunning),
|
||
allAgents: unref(allAgents),
|
||
getToolsForTurn: unref(getToolsForTurn),
|
||
hudVersion: unref(hudVersion)
|
||
}, null, _parent2, _scopeId));
|
||
} else {
|
||
_push2(`<!---->`);
|
||
}
|
||
if (i === lastRealMsgIdx.value) {
|
||
_push2(`<div class="sm-status-bar" data-v-8c19bd9d${_scopeId}><div class="${ssrRenderClass([smState.value, "sm-dot"])}" data-v-8c19bd9d${_scopeId}></div><span class="sm-status-label" data-v-8c19bd9d${_scopeId}>${ssrInterpolate(smLabel.value)}</span></div>`);
|
||
} else {
|
||
_push2(`<!---->`);
|
||
}
|
||
_push2(`<!--]-->`);
|
||
});
|
||
_push2(`<!--]-->`);
|
||
if (lastRealMsgIdx.value === -1) {
|
||
_push2(`<div class="sm-status-bar" data-v-8c19bd9d${_scopeId}><div class="${ssrRenderClass([smState.value, "sm-dot"])}" data-v-8c19bd9d${_scopeId}></div><span class="sm-status-label" data-v-8c19bd9d${_scopeId}>${ssrInterpolate(smLabel.value)}</span></div>`);
|
||
} else {
|
||
_push2(`<!---->`);
|
||
}
|
||
if (footerHeadline.value) {
|
||
_push2(ssrRenderComponent(SystemMessage, { msg: footerHeadline.value }, null, _parent2, _scopeId));
|
||
} else {
|
||
_push2(`<!---->`);
|
||
}
|
||
_push2(`<div class="msg-controls" data-v-8c19bd9d${_scopeId}>`);
|
||
_push2(ssrRenderComponent(HudControls, {
|
||
smState: smState.value,
|
||
connected: unref(connected),
|
||
isAgentRunning: unref(chatStore).isRunning,
|
||
handoverPending: unref(chatStore).handoverPending,
|
||
isPublic: unref(selectedMode) === "public",
|
||
onNew,
|
||
onHandover: ($event) => unref(chatStore).handover(),
|
||
onConfirmNew,
|
||
onStay: ($event) => unref(chatStore).stay()
|
||
}, null, _parent2, _scopeId));
|
||
_push2(`</div><div class="scroll-spacer" style="${ssrRenderStyle({ height: spacerHeight.value + "px", flexShrink: 0 })}" data-v-8c19bd9d${_scopeId}></div>`);
|
||
} else {
|
||
return [
|
||
prevSessions.value.length ? (openBlock(), createBlock(Fragment, { key: 0 }, [
|
||
prevHasMore.value ? (openBlock(), createBlock("button", {
|
||
key: 0,
|
||
class: "prev-load-more",
|
||
onClick: ($event) => fetchPreviousSession(true),
|
||
disabled: prevLoading.value
|
||
}, toDisplayString(prevLoading.value ? "Loading..." : "Load older session"), 9, ["onClick", "disabled"])) : createCommentVNode("", true),
|
||
(openBlock(true), createBlock(Fragment, null, renderList(prevSessions.value, (session, si) => {
|
||
return openBlock(), createBlock(Fragment, {
|
||
key: "ps-" + si
|
||
}, [
|
||
createVNode("div", { class: "prev-session-header" }, " Session " + toDisplayString(prevSessions.value.length - si) + toDisplayString(session.timeLabel ? " — " + session.timeLabel : ""), 1),
|
||
createVNode("div", { class: "prev-session-wrapper" }, [
|
||
(openBlock(true), createBlock(Fragment, null, renderList(session.messages, (msg, mi) => {
|
||
return openBlock(), createBlock(Fragment, {
|
||
key: "prev-" + si + "-" + mi
|
||
}, [
|
||
msg.role === "user" ? (openBlock(), createBlock(UserMessage, {
|
||
key: 0,
|
||
msg
|
||
}, null, 8, ["msg"])) : msg.role === "assistant" ? (openBlock(), createBlock(AssistantMessage, {
|
||
key: 1,
|
||
msg,
|
||
agentDisplayName: unref(getFormattedAgentName)(msg.agentId || unref(selectedAgent)),
|
||
isAgentRunning: false,
|
||
allAgents: unref(allAgents),
|
||
getToolsForTurn: () => [],
|
||
hudVersion: 0
|
||
}, null, 8, ["msg", "agentDisplayName", "allAgents"])) : createCommentVNode("", true)
|
||
], 64);
|
||
}), 128))
|
||
])
|
||
], 64);
|
||
}), 128)),
|
||
createVNode("div", { class: "session-divider" }, [
|
||
createVNode("span", { class: "session-divider-text" }, "Current session")
|
||
])
|
||
], 64)) : createCommentVNode("", true),
|
||
(openBlock(true), createBlock(Fragment, null, renderList(unref(groupedVisibleMsgs), (msg, i) => {
|
||
return openBlock(), createBlock(Fragment, { key: i }, [
|
||
msg.type === "no_session" ? (openBlock(), createBlock("div", {
|
||
key: 0,
|
||
class: "no-session-center"
|
||
}, "— NO SESSION —")) : createCommentVNode("", true),
|
||
msg.type !== "no_session" && (msg.role === "system" || msg.role === "system_group" || msg.type === "headline") && msg.position !== "footer" ? (openBlock(), createBlock(SystemMessage, {
|
||
key: 1,
|
||
msg
|
||
}, null, 8, ["msg"])) : createCommentVNode("", true),
|
||
unref(hasMore) && msg.type === "headline" && msg.position !== "footer" && i === 0 ? (openBlock(), createBlock("button", {
|
||
key: 2,
|
||
class: "load-more-btn",
|
||
onClick: unref(loadMore)
|
||
}, "↑ Load previous messages", 8, ["onClick"])) : msg.role === "session_context" ? (openBlock(), createBlock("div", {
|
||
key: 3,
|
||
class: "session-context-badge"
|
||
}, [
|
||
createVNode("span", { class: "session-context-text" }, toDisplayString(msg.content), 1)
|
||
])) : msg.role === "user" ? (openBlock(), createBlock(UserMessage, {
|
||
key: 4,
|
||
msg
|
||
}, null, 8, ["msg"])) : msg.role === "thinking" ? (openBlock(), createBlock("div", {
|
||
key: 5,
|
||
class: "message thinking"
|
||
}, [
|
||
createVNode("details", {
|
||
open: !msg.collapsed
|
||
}, [
|
||
createVNode("summary", null, [
|
||
createVNode(unref(ChatBubbleBottomCenterTextIcon), { class: "w-4 h-4 inline" }),
|
||
createTextVNode(" thinking…")
|
||
]),
|
||
createVNode("pre", { class: "thinking-content" }, toDisplayString(typeof msg.content === "object" ? msg.content.value : msg.content), 1)
|
||
], 8, ["open"])
|
||
])) : msg.role === "assistant" ? (openBlock(), createBlock(AssistantMessage, {
|
||
key: 6,
|
||
msg,
|
||
agentDisplayName: unref(getFormattedAgentName)(msg.agentId),
|
||
isAgentRunning: unref(isAgentRunning),
|
||
allAgents: unref(allAgents),
|
||
getToolsForTurn: unref(getToolsForTurn),
|
||
hudVersion: unref(hudVersion)
|
||
}, null, 8, ["msg", "agentDisplayName", "isAgentRunning", "allAgents", "getToolsForTurn", "hudVersion"])) : createCommentVNode("", true),
|
||
i === lastRealMsgIdx.value ? (openBlock(), createBlock("div", {
|
||
key: 7,
|
||
class: "sm-status-bar"
|
||
}, [
|
||
createVNode("div", {
|
||
class: ["sm-dot", smState.value]
|
||
}, null, 2),
|
||
createVNode("span", { class: "sm-status-label" }, toDisplayString(smLabel.value), 1)
|
||
])) : createCommentVNode("", true)
|
||
], 64);
|
||
}), 128)),
|
||
lastRealMsgIdx.value === -1 ? (openBlock(), createBlock("div", {
|
||
key: 1,
|
||
class: "sm-status-bar"
|
||
}, [
|
||
createVNode("div", {
|
||
class: ["sm-dot", smState.value]
|
||
}, null, 2),
|
||
createVNode("span", { class: "sm-status-label" }, toDisplayString(smLabel.value), 1)
|
||
])) : createCommentVNode("", true),
|
||
footerHeadline.value ? (openBlock(), createBlock(SystemMessage, {
|
||
key: 2,
|
||
msg: footerHeadline.value
|
||
}, null, 8, ["msg"])) : createCommentVNode("", true),
|
||
createVNode("div", {
|
||
class: "msg-controls",
|
||
ref_key: "controlsEl",
|
||
ref: controlsEl
|
||
}, [
|
||
createVNode(HudControls, {
|
||
smState: smState.value,
|
||
connected: unref(connected),
|
||
isAgentRunning: unref(chatStore).isRunning,
|
||
handoverPending: unref(chatStore).handoverPending,
|
||
isPublic: unref(selectedMode) === "public",
|
||
onNew,
|
||
onHandover: ($event) => unref(chatStore).handover(),
|
||
onConfirmNew,
|
||
onStay: ($event) => unref(chatStore).stay()
|
||
}, null, 8, ["smState", "connected", "isAgentRunning", "handoverPending", "isPublic", "onHandover", "onStay"])
|
||
], 512),
|
||
createVNode("div", {
|
||
class: "scroll-spacer",
|
||
style: { height: spacerHeight.value + "px", flexShrink: 0 }
|
||
}, null, 4)
|
||
];
|
||
}
|
||
}),
|
||
_: 1
|
||
}, _parent));
|
||
_push(`</div><div class="input-area" data-v-8c19bd9d>`);
|
||
if (unref(micDenied)) {
|
||
_push(`<div class="mic-denied-hint" data-v-8c19bd9d>Mic access denied — enable in browser settings</div>`);
|
||
} else {
|
||
_push(`<!---->`);
|
||
}
|
||
if (unref(isRecording)) {
|
||
_push(`<div class="recording-strip" data-v-8c19bd9d><span class="rec-dot" data-v-8c19bd9d></span><div class="rec-level-bar" data-v-8c19bd9d><div class="rec-level-fill" style="${ssrRenderStyle({ width: unref(audioLevel) * 100 + "%" })}" data-v-8c19bd9d></div></div><span class="rec-time" data-v-8c19bd9d>${ssrInterpolate(unref(formatDuration)(unref(duration)))}</span><button class="rec-cancel" title="Cancel" data-v-8c19bd9d>×</button></div>`);
|
||
} else {
|
||
_push(`<!---->`);
|
||
}
|
||
if (unref(attachments).length) {
|
||
_push(`<div class="attachment-strip" data-v-8c19bd9d><!--[-->`);
|
||
ssrRenderList(unref(attachments), (att, i) => {
|
||
_push(`<div class="${ssrRenderClass([{ "audio-thumb": att.mimeType.startsWith("audio/") }, "attachment-thumb"])}" data-v-8c19bd9d>`);
|
||
if (att.mimeType.startsWith("audio/")) {
|
||
_push(`<!--[--><span class="audio-icon" data-v-8c19bd9d>♫</span><span class="audio-name" data-v-8c19bd9d>${ssrInterpolate(att.fileName)}</span><!--]-->`);
|
||
} else {
|
||
_push(`<img${ssrRenderAttr("src", att.preview)}${ssrRenderAttr("alt", att.fileName)} data-v-8c19bd9d>`);
|
||
}
|
||
_push(`<button class="remove-att" data-v-8c19bd9d>×</button></div>`);
|
||
});
|
||
_push(`<!--]--></div>`);
|
||
} else {
|
||
_push(`<!---->`);
|
||
}
|
||
_push(`<div class="${ssrRenderClass([{ "drag-over": isDragOver.value }, "input-box"])}" data-v-8c19bd9d><button class="attach-btn" title="Attach file" data-v-8c19bd9d>`);
|
||
_push(ssrRenderComponent(unref(PaperClipIcon), { class: "w-4 h-4" }, null, _parent));
|
||
_push(`</button><button class="${ssrRenderClass([{ recording: unref(isRecording) }, "mic-btn"])}"${ssrRenderAttr("title", unref(isRecording) ? "Stop recording" : "Record audio")} data-v-8c19bd9d>`);
|
||
_push(ssrRenderComponent(unref(MicrophoneIcon), { class: "w-4 h-4" }, null, _parent));
|
||
_push(`</button><input type="file" class="hidden" accept="image/jpeg,image/png,image/gif,image/webp,application/pdf,audio/webm,audio/mp4,audio/ogg,audio/mpeg,audio/wav" multiple data-v-8c19bd9d><textarea placeholder="Message..." rows="1" class="${ssrRenderClass([{ shake: unref(isShaking) }, "chat-input"])}" data-v-8c19bd9d>${ssrInterpolate(unref(input))}</textarea>`);
|
||
if (smState.value === "AGENT_RUNNING" || smState.value === "STOP_PENDING") {
|
||
_push(`<button class="stop-btn"${ssrIncludeBooleanAttr(smState.value === "STOP_PENDING") ? " disabled" : ""}${ssrRenderAttr("title", smState.value === "STOP_PENDING" ? "Stopping..." : "Stop")} data-v-8c19bd9d>`);
|
||
_push(ssrRenderComponent(unref(StopIcon), { class: "w-3.5 h-3.5" }, null, _parent));
|
||
_push(`</button>`);
|
||
} else {
|
||
_push(`<!---->`);
|
||
}
|
||
_push(`<button class="send-btn"${ssrIncludeBooleanAttr(!unref(connected) || unref(sending) || !unref(input).trim() && !unref(hasAttachments)()) ? " disabled" : ""} data-v-8c19bd9d>`);
|
||
if (!unref(sending)) {
|
||
_push(ssrRenderComponent(unref(ArrowUpIcon), { class: "w-4 h-4" }, null, _parent));
|
||
} else {
|
||
_push(`<span style="${ssrRenderStyle({ "font-size": "11px" })}" data-v-8c19bd9d>…</span>`);
|
||
}
|
||
_push(`</button></div></div></div></div></div>`);
|
||
} else {
|
||
_push(`<div${ssrRenderAttrs(mergeProps({ class: "not-logged-in" }, _attrs))} data-v-8c19bd9d><p data-v-8c19bd9d>`);
|
||
_push(ssrRenderComponent(unref(LockClosedIcon), { class: "w-5 h-5 inline" }, null, _parent));
|
||
_push(` Not logged in</p>`);
|
||
_push(ssrRenderComponent(_component_RouterLink, { to: "/login" }, {
|
||
default: withCtx((_, _push2, _parent2, _scopeId) => {
|
||
if (_push2) {
|
||
_push2(`Sign in →`);
|
||
} else {
|
||
return [
|
||
createTextVNode("Sign in →")
|
||
];
|
||
}
|
||
}),
|
||
_: 1
|
||
}, _parent));
|
||
_push(`</div>`);
|
||
}
|
||
};
|
||
}
|
||
});
|
||
const _sfc_setup = _sfc_main.setup;
|
||
_sfc_main.setup = (props, ctx) => {
|
||
const ssrContext = useSSRContext();
|
||
(ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("src/views/AgentsView.vue");
|
||
return _sfc_setup ? _sfc_setup(props, ctx) : void 0;
|
||
};
|
||
const AgentsView = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-8c19bd9d"]]);
|
||
export {
|
||
AgentsView as default
|
||
};
|