From 388940aae33c99327d07e9bf2ea52d1a9091dc2f Mon Sep 17 00:00:00 2001 From: "Maksym Hryzodub [DREAM]" Date: Wed, 26 Aug 2026 12:54:35 +0300 Subject: [PATCH 1/4] feat(chat): redesign chat history pages in admin and app (CLEAN-47) Two-column layout with a slim header and right rail (meta, summary, navigation mini-map, export), reworked bubbles per the design mockup. Admin groups tool_call/tool_result pairs client-side and attaches them as collapsible rows above the assistant reply they belong to. Colors go through theme tokens so dark mode keeps working. Co-Authored-By: Claude Opus 4.7 --- .../chat/components/chat/detail/MetaCard.vue | 48 ++++ .../chat/components/chat/detail/NavMap.vue | 34 +++ .../chat/components/chat/detail/Provider.vue | 143 +++++++---- .../components/chat/detail/SummaryCard.vue | 106 ++++---- .../chat/components/chat/message/Bubble.vue | 107 ++++---- .../components/chat/message/ToolEvents.vue | 67 +++++ admin/slices/chat/utils/transcript.ts | 118 +++++++++ app/i18n.sync.json | 6 + .../chat/components/chat/detail/NavMap.vue | 34 +++ .../chat/components/chat/detail/Provider.vue | 235 ++++++++++-------- .../chat/components/chat/message/Bubble.vue | 110 ++++---- app/slices/chat/i18n/locales/en.json | 10 +- app/slices/chat/i18n/locales/ru.json | 10 +- app/slices/chat/utils/transcript.ts | 27 ++ 14 files changed, 757 insertions(+), 298 deletions(-) create mode 100644 admin/slices/chat/components/chat/detail/MetaCard.vue create mode 100644 admin/slices/chat/components/chat/detail/NavMap.vue create mode 100644 admin/slices/chat/components/chat/message/ToolEvents.vue create mode 100644 admin/slices/chat/utils/transcript.ts create mode 100644 app/slices/chat/components/chat/detail/NavMap.vue create mode 100644 app/slices/chat/utils/transcript.ts diff --git a/admin/slices/chat/components/chat/detail/MetaCard.vue b/admin/slices/chat/components/chat/detail/MetaCard.vue new file mode 100644 index 0000000..0df697b --- /dev/null +++ b/admin/slices/chat/components/chat/detail/MetaCard.vue @@ -0,0 +1,48 @@ + + + diff --git a/admin/slices/chat/components/chat/detail/NavMap.vue b/admin/slices/chat/components/chat/detail/NavMap.vue new file mode 100644 index 0000000..ecb84bd --- /dev/null +++ b/admin/slices/chat/components/chat/detail/NavMap.vue @@ -0,0 +1,34 @@ + + + diff --git a/admin/slices/chat/components/chat/detail/Provider.vue b/admin/slices/chat/components/chat/detail/Provider.vue index 703e6b1..f9ea54e 100644 --- a/admin/slices/chat/components/chat/detail/Provider.vue +++ b/admin/slices/chat/components/chat/detail/Provider.vue @@ -1,5 +1,10 @@ diff --git a/admin/slices/chat/components/chat/detail/SummaryCard.vue b/admin/slices/chat/components/chat/detail/SummaryCard.vue index 3efa7a2..398787d 100644 --- a/admin/slices/chat/components/chat/detail/SummaryCard.vue +++ b/admin/slices/chat/components/chat/detail/SummaryCard.vue @@ -28,72 +28,58 @@ const sentimentVariant: Record props.session.title || props.session.externalUserId || '—', -); diff --git a/admin/slices/chat/components/chat/message/Bubble.vue b/admin/slices/chat/components/chat/message/Bubble.vue index 1a0ad5e..a140b9e 100644 --- a/admin/slices/chat/components/chat/message/Bubble.vue +++ b/admin/slices/chat/components/chat/message/Bubble.vue @@ -1,19 +1,24 @@ + + diff --git a/admin/slices/chat/utils/transcript.ts b/admin/slices/chat/utils/transcript.ts new file mode 100644 index 0000000..38750a4 --- /dev/null +++ b/admin/slices/chat/utils/transcript.ts @@ -0,0 +1,118 @@ +import type { IChatMessage } from '#chat/stores/chat'; + +export interface IToolEvent { + id: string; + name: string; + args: string; + result: string | null; + durationMs: number | null; +} + +export interface INavMapItem { + id: string; + isUser: boolean; + snippet: string; +} + +export interface ITranscriptItem { + key: string; + /** null → standalone tool events with no assistant reply after them */ + message: IChatMessage | null; + tools: IToolEvent[]; +} + +// Transcript reader renders tool_call as `name({...params})`. +const CALL_RE = /^([\w.$:-]+)\((.*)\)$/s; + +/** + * Groups a flat transcript for display: consecutive tool_call/tool_result + * events pair up by order and attach to the next assistant reply — mirroring + * how the agent produced them. Orphans (tools with no reply after, results + * with no call) still surface as standalone items so nothing is hidden. + */ +export function groupTranscript(messages: IChatMessage[]): ITranscriptItem[] { + const items: ITranscriptItem[] = []; + let pending: { evt: IToolEvent; callTs: number; done: boolean }[] = []; + + function flushPending() { + if (!pending.length) return; + items.push({ + key: `tools-${pending[0]!.evt.id}`, + message: null, + tools: pending.map((p) => p.evt), + }); + pending = []; + } + + for (const m of messages) { + if (m.role === 'tool_call') { + const parsed = CALL_RE.exec(m.text.trim()); + pending.push({ + evt: { + id: m.id, + name: parsed?.[1] ?? 'tool', + args: parsed?.[2] ?? m.text, + result: null, + durationMs: null, + }, + callTs: m.ts, + done: false, + }); + continue; + } + if (m.role === 'tool_result') { + const open = pending.find((p) => !p.done); + if (open) { + open.evt.result = m.text; + open.evt.durationMs = Math.max(0, m.ts - open.callTs); + open.done = true; + } else { + pending.push({ + evt: { id: m.id, name: 'tool', args: '', result: m.text, durationMs: null }, + callTs: m.ts, + done: true, + }); + } + continue; + } + if (m.role === 'assistant') { + items.push({ key: m.id, message: m, tools: pending.map((p) => p.evt) }); + pending = []; + continue; + } + // user / summary / system: tools never span across these — flush first + flushPending(); + items.push({ key: m.id, message: m, tools: [] }); + } + flushPending(); + return items; +} + +export function formatDuration(ms: number | null): string { + if (ms === null) return ''; + if (ms < 10_000) return `${(ms / 1000).toFixed(1)}s`; + if (ms < 60_000) return `${Math.round(ms / 1000)}s`; + return `${Math.floor(ms / 60_000)}m ${Math.round((ms % 60_000) / 1000)}s`; +} + +export function formatMessageTime(ts: number): string { + const d = new Date(ts); + const time = d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); + const now = new Date(); + const sameDay = + d.getFullYear() === now.getFullYear() && + d.getMonth() === now.getMonth() && + d.getDate() === now.getDate(); + if (sameDay) return time; + return `${d.toLocaleDateString([], { day: 'numeric', month: 'short' })}, ${time}`; +} + +/** One-line plain-text snippet for the navigation mini-map. */ +export function snippet(text: string, max = 64): string { + const line = + text + .split('\n') + .map((l) => l.replace(/[#*_`>~-]/g, '').trim()) + .find((l) => l.length > 0) ?? ''; + return line.length > max ? `${line.slice(0, max)}…` : line; +} diff --git a/app/i18n.sync.json b/app/i18n.sync.json index 758c4bf..31e0798 100644 --- a/app/i18n.sync.json +++ b/app/i18n.sync.json @@ -62,6 +62,8 @@ "history.sync": "8d261a372fde", "history.syncing": "8a046cc90ab0", "history.title": "0e7696009337", + "message.copied": "8d525e5f158b", + "message.copy": "e21f935f11d7", "message.helpful": "63c432db3ebb", "message.hide": "ac20a57bfde0", "message.not_helpful": "3c4ebf31165b", @@ -74,10 +76,14 @@ "session.load_older": "2fd15b734915", "session.loading": "ba3bbbe10d8b", "session.message_count": "ff43d682a3fb", + "session.meta_activity": "06475633ed3e", + "session.meta_messages": "04d7b4833927", + "session.navigation": "3db65f8c2a7d", "session.no_messages": "1e56e882e6e4", "session.no_summary": "f098ecd68006", "session.not_found_hint": "ca81b79f7f6d", "session.not_found_title": "d8e4dcef4f3d", + "session.refresh": "0e9161011702", "session.resolved": "dc676b428599", "session.unresolved": "27a888c9d5e6" }, diff --git a/app/slices/chat/components/chat/detail/NavMap.vue b/app/slices/chat/components/chat/detail/NavMap.vue new file mode 100644 index 0000000..6a30c5c --- /dev/null +++ b/app/slices/chat/components/chat/detail/NavMap.vue @@ -0,0 +1,34 @@ + + + diff --git a/app/slices/chat/components/chat/detail/Provider.vue b/app/slices/chat/components/chat/detail/Provider.vue index 6782d20..53e8950 100644 --- a/app/slices/chat/components/chat/detail/Provider.vue +++ b/app/slices/chat/components/chat/detail/Provider.vue @@ -1,5 +1,6 @@