@twentyfourg/chat-kit 1.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +222 -0
- package/dist/chat-kit.js +1914 -0
- package/dist/chat-kit.js.map +1 -0
- package/dist/components/AssistantMessage.vue.d.ts +35 -0
- package/dist/components/Chat.vue.d.ts +72 -0
- package/dist/components/CitationPopover.vue.d.ts +11 -0
- package/dist/components/FeedbackRow.vue.d.ts +11 -0
- package/dist/components/IconButton.vue.d.ts +17 -0
- package/dist/components/InputBar.vue.d.ts +16 -0
- package/dist/components/KitIcon.vue.d.ts +12 -0
- package/dist/components/MessageList.vue.d.ts +36 -0
- package/dist/components/PageImageModal.vue.d.ts +14 -0
- package/dist/components/SourceCard.vue.d.ts +21 -0
- package/dist/components/SourceList.vue.d.ts +7 -0
- package/dist/components/ThinkingAnimation.vue.d.ts +15 -0
- package/dist/components/ThinkingIndicator.vue.d.ts +13 -0
- package/dist/components/ThinkingProcess.vue.d.ts +8 -0
- package/dist/components/UserMessage.vue.d.ts +7 -0
- package/dist/composables/useAutoScroll.d.ts +36 -0
- package/dist/composables/useChatEngine.d.ts +335 -0
- package/dist/composables/useDictation.d.ts +29 -0
- package/dist/copy.d.ts +144 -0
- package/dist/index.css +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/services/anonymous-auth.d.ts +63 -0
- package/dist/services/base-camp-client.d.ts +66 -0
- package/dist/services/citations.d.ts +18 -0
- package/dist/services/entities.d.ts +29 -0
- package/dist/services/markdown.d.ts +47 -0
- package/dist/services/math.d.ts +36 -0
- package/dist/services/streaming-message.d.ts +32 -0
- package/dist/services/streaming.service.d.ts +84 -0
- package/dist/services/typewriter.d.ts +54 -0
- package/dist/testing/mock-transport.d.ts +52 -0
- package/dist/testing/setup-tests.d.ts +8 -0
- package/dist/theme.css +122 -0
- package/dist/types.d.ts +389 -0
- package/package.json +76 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chat-kit.js","names":["$slots","$slots"],"sources":["../src/copy.ts","../src/services/anonymous-auth.ts","../src/services/base-camp-client.ts","../src/services/streaming.service.ts","../src/services/math.ts","../src/services/markdown.ts","../src/services/entities.ts","../src/services/streaming-message.ts","../src/services/typewriter.ts","../src/composables/useChatEngine.ts","../src/testing/mock-transport.ts","../src/composables/useDictation.ts","../src/components/IconButton.vue","../src/components/IconButton.vue","../src/components/KitIcon.vue","../src/components/KitIcon.vue","../src/components/InputBar.vue","../src/components/InputBar.vue","../src/composables/useAutoScroll.ts","../src/services/citations.ts","../src/components/CitationPopover.vue","../src/components/CitationPopover.vue","../src/components/FeedbackRow.vue","../src/components/FeedbackRow.vue","../src/components/PageImageModal.vue","../src/components/PageImageModal.vue","../src/components/SourceCard.vue","../src/components/SourceCard.vue","../src/components/SourceList.vue","../src/components/SourceList.vue","../src/components/ThinkingAnimation.vue","../src/components/ThinkingAnimation.vue","../src/components/ThinkingProcess.vue","../src/components/ThinkingProcess.vue","../src/components/AssistantMessage.vue","../src/components/AssistantMessage.vue","../src/components/ThinkingIndicator.vue","../src/components/ThinkingIndicator.vue","../src/components/UserMessage.vue","../src/components/UserMessage.vue","../src/components/MessageList.vue","../src/components/MessageList.vue","../src/components/Chat.vue","../src/components/Chat.vue"],"sourcesContent":["/**\n * All the wording the chat shows, so an app using this kit can supply its own\n * without editing any component.\n *\n * The chat takes one `copy` prop holding an object shaped like ChatCopy. Leave\n * anything out and the English text below is used instead, with a warning in\n * development listing what was missing. Components inside the chat read the\n * finished object with useChatCopy(), so it is not passed down by hand.\n *\n * The object is plain strings only, no functions, so it can also be written as\n * a JSON file. Where a string needs a value dropped into it, write {name} and\n * render it with formatCopy(). Apps writing it in TypeScript can add\n * `satisfies ChatCopy` to catch anything missing at build time.\n */\nimport { inject, provide, type InjectionKey } from 'vue';\n\nexport interface ChatCopy {\n /**\n * 'welcome' is shown before the first message is sent. Apps that want more than text here,\n * such as a logo or buttons, use the chat's welcome slot instead.\n */\n welcome: {\n eyebrow: string;\n heading: string;\n body: string;\n };\n /** The names shown above each message. */\n roles: {\n assistant: string;\n user: string;\n };\n composer: {\n placeholder: string;\n /** Tooltip on the mic spot when the browser has no speech recognition. */\n micUnsupported: string;\n /** Names of the composer's icon-only buttons, shown on hover. */\n dictateLabel: string;\n sendLabel: string;\n stopLabel: string;\n };\n status: {\n /** Held beside the waiting animation. Leave empty to show the animation alone. */\n thinking: string;\n error: string;\n retryLabel: string;\n /**\n * Shown when the server refuses a message because the sender is over\n * their message limit. Optional: an app that does not set it shows its\n * own `error` string for these too, exactly as before this key existed.\n */\n rateLimited?: string;\n /**\n * Lines the thinking indicator steps through while an answer is being\n * retrieved, in place of the single `thinking` label. Only read when the\n * chat's `rotateThinkingLabel` prop is on. An empty array falls back to\n * `thinking`, which is also how to switch the sequence off per-app.\n */\n thinkingSequence?: string[];\n };\n feedback: {\n question: string;\n };\n sources: {\n openSourceLabel: string;\n /** Heading over the quoted passage in a citation popover. */\n citedPassageLabel: string;\n /** Caveat shown with a source, such as a login the reader may need. Leave empty to hide it. */\n noticeLine: string;\n /** The copy-link button in a citation popover, and what it says right after copying. */\n copyUrlLabel: string;\n copiedLabel: string;\n /**\n * The rest of this section belongs to the source cards that list mode\n * shows under an answer. All optional; see ResolvedChatCopy.\n */\n /** Line above the cards. Leave empty to hide it. */\n introLine?: string;\n /** Numbered heading on each card; {n} is the card's position. */\n sourceLabel?: string;\n /** Heading over a card's cited passages. */\n referenceLabel?: string;\n /** Where a passage sits in the document; {page} is the page number. */\n pageLabel?: string;\n /** Appended to pageLabel when the printed page differs; {textPage}. */\n textPageLabel?: string;\n /** Names of the card's fold toggle, and of the page-image dialog's close button. */\n expandLabel?: string;\n collapseLabel?: string;\n closeLabel?: string;\n };\n /** Shown when the assistant declines a question it cannot answer. */\n outOfScope: {\n label: string;\n };\n /**\n * The collapsible panel showing the model's reasoning. Only rendered when\n * the app turns on the chat's `showThinking` prop, so the section is\n * optional here too.\n */\n reasoning?: {\n showLabel: string;\n hideLabel: string;\n };\n /**\n * The row of actions under a finished answer. Only the copy button uses\n * these, and only when the app turns on `showCopy`, so the section is\n * optional here too.\n */\n actions?: {\n /** Names the button that copies the answer. */\n copyLabel: string;\n /** Replaces it for a moment afterwards, since nothing else visibly happens. */\n copiedLabel: string;\n };\n}\n\n/**\n * Copy with every later-added optional part guaranteed present. This is what\n * resolveChatCopy returns and what components read through useChatCopy, so a\n * component never carries a fallback for a string the defaults already have.\n *\n * The optional keys exist because apps were already shipping complete maps\n * with `satisfies ChatCopy` when these strings were added; making them\n * required would have broken those builds.\n */\nexport type ResolvedChatCopy = ChatCopy & {\n status: ChatCopy['status'] & { rateLimited: string; thinkingSequence: string[] };\n sources: ChatCopy['sources'] & {\n introLine: string;\n sourceLabel: string;\n referenceLabel: string;\n pageLabel: string;\n textPageLabel: string;\n expandLabel: string;\n collapseLabel: string;\n closeLabel: string;\n };\n reasoning: NonNullable<ChatCopy['reasoning']>;\n actions: NonNullable<ChatCopy['actions']>;\n};\n\ntype DeepPartial<T> = { [K in keyof T]?: DeepPartial<T[K]> };\n\n/** What an app passes to the chat's `copy` prop. Any part can be left out. */\nexport type ChatCopyInput = DeepPartial<ChatCopy>;\n\nexport const defaultChatCopy: ResolvedChatCopy = {\n welcome: {\n eyebrow: '',\n heading: 'How can I help?',\n body: 'Ask a question to get started.',\n },\n roles: {\n assistant: 'Assistant',\n user: 'You',\n },\n composer: {\n placeholder: 'Ask a question...',\n micUnsupported: 'Voice input is not supported in this browser.',\n dictateLabel: 'Dictate',\n sendLabel: 'Send',\n stopLabel: 'Stop',\n },\n status: {\n thinking: 'Thinking…',\n error: 'Something went wrong while generating a response. Please try again.',\n retryLabel: 'Retry',\n // Deliberately the same as `error`: distinct rate-limit wording is purely\n // opt-in, so apps written before this key show exactly what they always did.\n rateLimited: 'Something went wrong while generating a response. Please try again.',\n // A starting point, not a house style. Apps with an assistant name or a\n // named corpus should say so here instead.\n thinkingSequence: [\n 'Analyzing your question…',\n 'Working to understand your question and finding the most relevant content…',\n 'Processing your query…',\n 'Searching the knowledge base…',\n 'Connecting related concepts across the material…',\n ],\n },\n feedback: {\n question: 'Was this helpful?',\n },\n sources: {\n openSourceLabel: 'Open source',\n citedPassageLabel: 'Cited passage',\n noticeLine: '',\n copyUrlLabel: 'Copy URL',\n copiedLabel: 'Copied!',\n introLine: '',\n sourceLabel: 'Source {n}',\n referenceLabel: 'References',\n pageLabel: 'Page {page}',\n textPageLabel: 'Text page {textPage}',\n expandLabel: 'Show source details',\n collapseLabel: 'Hide source details',\n closeLabel: 'Close',\n },\n outOfScope: {\n label: 'Outside this assistant’s scope',\n },\n reasoning: {\n showLabel: 'Show thinking',\n hideLabel: 'Hide thinking',\n },\n actions: {\n copyLabel: 'Copy answer',\n copiedLabel: 'Copied!',\n },\n};\n\n/** Drops values into a string, so formatCopy('{n} sources', { n: 3 }) gives '3 sources'. */\nexport function formatCopy(template: string, params: Record<string, string | number>): string {\n return template.replace(/\\{(\\w+)\\}/g, (match, key: string) =>\n key in params ? String(params[key]) : match,\n );\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\n/**\n * Strings added after apps were already shipping complete copy maps. A map\n * written before these existed is not missing anything, so they are excluded\n * from the development warning below.\n */\nconst ADDED_AFTER_FIRST_RELEASE = new Set([\n 'status.rateLimited',\n 'status.thinkingSequence',\n 'sources.introLine',\n 'sources.sourceLabel',\n 'sources.referenceLabel',\n 'sources.pageLabel',\n 'sources.textPageLabel',\n 'sources.expandLabel',\n 'sources.collapseLabel',\n 'sources.closeLabel',\n 'reasoning.showLabel',\n 'reasoning.hideLabel',\n 'actions.copyLabel',\n 'actions.copiedLabel',\n]);\n\n/**\n * Merges provided copy with the default values. Any missing strings fall back to the default and gets listed in a warning\n * during development. That way an incomplete set of text is easy to spot but never leaves blank labels in the interface.\n */\nexport function resolveChatCopy(input?: ChatCopyInput): ResolvedChatCopy {\n const missing: string[] = [];\n\n function merge<T>(defaults: T, provided: unknown, path: string): T {\n // Arrays are leaves: an app's list replaces the default wholesale rather\n // than merging position by position. Both are copied so the resolved map\n // never shares an array with defaultChatCopy or with the caller.\n if (Array.isArray(defaults)) {\n if (Array.isArray(provided)) return [...provided] as T;\n missing.push(path);\n return [...defaults] as T;\n }\n if (isPlainObject(defaults)) {\n const result = {} as T;\n for (const key of Object.keys(defaults) as (keyof T & string)[]) {\n const childProvided = isPlainObject(provided) ? provided[key] : undefined;\n result[key] = merge(defaults[key], childProvided, path ? `${path}.${key}` : key);\n }\n return result;\n }\n if (typeof provided === typeof defaults) return provided as T;\n missing.push(path);\n return defaults;\n }\n\n const resolved = merge(defaultChatCopy, input, '');\n\n // An app that never wrote rate-limit copy keeps showing its own generic\n // error for those failures, exactly as it did before this key existed.\n if (input?.status?.rateLimited === undefined) {\n resolved.status.rateLimited = resolved.status.error;\n }\n\n const isDev = (import.meta as { env?: { DEV?: boolean } }).env?.DEV === true;\n\n const required = missing.filter((path) => !ADDED_AFTER_FIRST_RELEASE.has(path));\n if (required.length > 0 && input !== undefined && isDev) {\n console.warn(\n `[chat-kit] Copy map is missing ${required.length} string(s); using defaults for: ${required.join(', ')}`,\n );\n }\n\n return resolved;\n}\n\nconst ChatCopyKey: InjectionKey<ResolvedChatCopy> = Symbol('chat-copy');\n\n/** Called once by the chat root component with its `copy` prop value. */\nexport function provideChatCopy(input?: ChatCopyInput): ResolvedChatCopy {\n const resolved = resolveChatCopy(input);\n provide(ChatCopyKey, resolved);\n return resolved;\n}\n\n/** Read the resolved copy anywhere inside the chat component tree. */\nexport function useChatCopy(): ResolvedChatCopy {\n return inject(ChatCopyKey, defaultChatCopy);\n}\n","/**\n * Signs the visitor in without asking them for anything.\n *\n * Base Camp will not answer without an identity, but this product is open to\n * the public. LXS provides an \"anonymous\" login for exactly this: the browser\n * asks for a throwaway session, then trades it for a token Base Camp accepts.\n * Nothing here is secret, so it runs in the browser with no server of ours in\n * the middle.\n *\n * The exchange happens in two steps, and the second proves the first came from\n * this same page:\n * 1. Ask for a session, sending a fingerprint of a random string.\n * 2. Hand back the original random string to collect the token.\n */\n\nexport interface AnonymousAuthOptions {\n /** Base URL of the LXS API. */\n lxsApiUrl: string;\n /**\n * Which login configuration to use. Optional: when left out, the tenant's\n * default one is looked up instead, which is what the LXP frontend does.\n */\n authConfigId?: string;\n /** Identifies the tenant these sessions belong to. */\n tenantNumber: string;\n deployEnv?: string;\n jobNumber?: string;\n}\n\ninterface AuthConfig {\n PK: string;\n authenticationStrategies?: { id: string; type: string; enabled: boolean }[];\n}\n\n/**\n * Asks LXS which login configuration applies to this tenant. The endpoint is\n * public, so it works before anyone has signed in. Saves having to hardcode an\n * id per tenant, and lets us check that guest sign-in is actually switched on.\n */\nexport async function fetchAuthConfig(options: AnonymousAuthOptions): Promise<AuthConfig> {\n const url = withEnvParams(\n new URL(`${options.lxsApiUrl}/api/v1/directory/authentication`),\n options,\n );\n const response = await fetch(url.toString(), {\n headers: { Accept: 'application/json', 'x-tenant-hint': options.tenantNumber },\n });\n if (!response.ok) {\n throw new AnonymousAuthError(`Could not read the login settings (${response.status}).`);\n }\n return (await response.json()) as AuthConfig;\n}\n\n/** A random string, kept only long enough to complete the exchange. */\nfunction randomVerifier(): string {\n const bytes = new Uint8Array(32);\n crypto.getRandomValues(bytes);\n return Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');\n}\n\n/**\n * The fingerprint sent in step one. 24G's login expects the hash written as\n * hexadecimal, which is not what most guides show, so do not swap this for a\n * base64 version.\n */\nasync function fingerprint(verifier: string): Promise<string> {\n const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier));\n return Array.from(new Uint8Array(digest), (b) => b.toString(16).padStart(2, '0')).join('');\n}\n\nfunction withEnvParams(url: URL, options: AnonymousAuthOptions): URL {\n if (options.deployEnv) url.searchParams.set('deployEnv', options.deployEnv);\n if (options.jobNumber) url.searchParams.set('jobNumber', options.jobNumber);\n return url;\n}\n\nexport class AnonymousAuthError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'AnonymousAuthError';\n }\n}\n\n/** Runs the two-step exchange and returns a token Base Camp will accept. */\nexport async function signInAnonymously(options: AnonymousAuthOptions): Promise<string> {\n const { lxsApiUrl, tenantNumber } = options;\n\n // Look the configuration up when we were not told which one to use.\n let authConfigId = options.authConfigId;\n if (!authConfigId) {\n const config = await fetchAuthConfig(options);\n const guest = config.authenticationStrategies?.find((s) => s.type === 'anonymous');\n if (!guest?.enabled) {\n throw new AnonymousAuthError('This tenant does not allow guest sign-in.');\n }\n authConfigId = config.PK;\n }\n\n const verifier = randomVerifier();\n const codeChallenge = await fingerprint(verifier);\n const headers = {\n Accept: 'application/json',\n 'Content-Type': 'application/json',\n 'x-tenant-hint': tenantNumber,\n };\n\n // Step one: ask for a throwaway session.\n const loginUrl = withEnvParams(\n new URL(`${lxsApiUrl}/api/v1/directory/authentication/${authConfigId}/anonymous/login`),\n options,\n );\n loginUrl.searchParams.set('codeChallenge', codeChallenge);\n loginUrl.searchParams.set('codeChallengeMethod', 'S256');\n\n const loginResponse = await fetch(loginUrl.toString(), {\n method: 'POST',\n headers,\n body: JSON.stringify({\n codeChallenge,\n codeChallengeMethod: 'S256',\n }),\n });\n\n if (!loginResponse.ok) {\n throw new AnonymousAuthError(`Anonymous sign-in failed (${loginResponse.status}).`);\n }\n\n const login = (await loginResponse.json()) as {\n loggingInSessionId?: string;\n sessionToken?: string;\n };\n if (!login.loggingInSessionId) {\n throw new AnonymousAuthError('Anonymous sign-in did not return a session.');\n }\n\n // Step two: prove it was us, and collect the token for Base Camp.\n const signUrl = withEnvParams(\n new URL(`${lxsApiUrl}/api/v1/directory/session/${encodeURIComponent(login.loggingInSessionId)}/sign`),\n options,\n );\n const signResponse = await fetch(signUrl.toString(), {\n method: 'POST',\n headers,\n body: JSON.stringify({ codeVerifier: verifier }),\n });\n\n if (!signResponse.ok) {\n throw new AnonymousAuthError(`Could not get a chat token (${signResponse.status}).`);\n }\n\n const signed = (await signResponse.json()) as {\n success?: boolean;\n baseCampAuthToken?: string;\n token?: string;\n message?: string;\n };\n const token = signed.baseCampAuthToken || signed.token;\n if (signed.success === false || !token) {\n throw new AnonymousAuthError(signed.message || 'No chat token was returned.');\n }\n\n return token;\n}\n\n/** What the Base Camp client asks for, with a way to start it early. */\nexport interface AnonymousTokenProvider {\n (): Promise<string>;\n /**\n * Signs in now rather than waiting for the first request. Call it once the\n * page has loaded so the exchange finishes while the visitor is still\n * reading, instead of delaying their first question. Failures are ignored\n * here: the next real request tries again and reports properly.\n */\n prefetch(): void;\n}\n\n/**\n * Wraps the sign-in so the token is fetched once and shared. Hand the returned\n * function to the Base Camp client, which calls it before every request.\n */\nexport function createAnonymousTokenProvider(\n options: AnonymousAuthOptions,\n): AnonymousTokenProvider {\n let pending: Promise<string> | null = null;\n\n const provider = (() => {\n if (!pending) {\n pending = signInAnonymously(options).catch((error) => {\n // Let the next request try again rather than failing forever.\n pending = null;\n throw error;\n });\n }\n return pending;\n }) as AnonymousTokenProvider;\n\n provider.prefetch = () => {\n provider().catch(() => {});\n };\n\n return provider;\n}\n","/**\n * Framework-free Base Camp API client built on fetch. Paths and shapes follow\n * Base Camp's Zodios API definitions (the authoritative OpenAPI contract).\n * No retries and no error swallowing; callers decide how to recover.\n */\n\nimport type {\n AgentDetails,\n CreateThreadBody,\n CreateThreadResponse,\n FeedbackResponse,\n MessageListResponse,\n QuotaResponse,\n SendMessageBody,\n StopMessageResponse,\n Thread,\n ThreadDetail,\n TrackEventBody,\n TrackEventResponse,\n} from '../types';\n\nexport interface BaseCampClientOptions {\n /** Base Camp REST host, without a trailing slash; request paths start with /v1. */\n baseUrl: string;\n /** Streaming host when it differs from the REST host; falls back to baseUrl. */\n streamBaseUrl?: string;\n /** Returns the Authorization credential for the current user. */\n getAuthToken: () => string | Promise<string>;\n /** Value for the x-24g-deploy-env identity header. */\n deployEnv: string;\n /** Value for the x-24g-job-number identity header. */\n jobNumber: string;\n /** Non-prod tenant override sent as x-tenant-hint when provided. */\n tenantHint?: string;\n}\n\n/** Normalized Base Camp API error. */\nexport class BaseCampError extends Error {\n readonly status: number;\n\n /** Machine-readable code when the server provides one, e.g. CONTEXT_LIMIT_EXCEEDED. */\n readonly code?: string;\n\n constructor(status: number, message: string, code?: string) {\n super(message);\n this.name = 'BaseCampError';\n this.status = status;\n this.code = code;\n }\n}\n\ninterface ThreadListResponse {\n threads: Thread[];\n}\n\nexport class BaseCampClient {\n private readonly options: BaseCampClientOptions;\n\n constructor(options: BaseCampClientOptions) {\n this.options = options;\n }\n\n /** GET /v1/agents/:agentId (public-safe agent detail; drive feature UI off config). */\n getAgent(agentId: string): Promise<AgentDetails> {\n return this.request<AgentDetails>('GET', `/v1/agents/${agentId}`);\n }\n\n /**\n * POST /v1/agents/:agentId/threads. The create endpoint only accepts variables;\n * when customData is supplied it is applied with a follow-up PATCH, since the\n * server only reads customData on thread update.\n */\n async createThread(agentId: string, body?: CreateThreadBody): Promise<CreateThreadResponse> {\n const { variables, customData } = body ?? {};\n const thread = await this.request<CreateThreadResponse>(\n 'POST',\n `/v1/agents/${agentId}/threads`,\n variables ? { variables } : {},\n );\n if (customData && Object.keys(customData).length > 0) {\n await this.request('PATCH', `/v1/agents/${agentId}/threads/${thread.id}`, { customData });\n }\n return thread;\n }\n\n /** GET /v1/agents/:agentId/threads (current user's threads for the agent). */\n /** GET /v1/agents/:agentId/threads?limit=N. Every status comes back; callers filter. */\n async getThreads(agentId: string, limit: number): Promise<Thread[]> {\n const response = await this.request<ThreadListResponse>(\n 'GET',\n `/v1/agents/${agentId}/threads?limit=${limit}`,\n );\n return response.threads;\n }\n\n /** GET /v1/agents/:agentId/threads/:threadId (includes embedded agent summary and customData). */\n getThread(agentId: string, threadId: string): Promise<ThreadDetail> {\n return this.request<ThreadDetail>('GET', `/v1/agents/${agentId}/threads/${threadId}`);\n }\n\n /** GET .../messages (full history; also the reconnect path after an interrupted stream). */\n getMessages(agentId: string, threadId: string): Promise<MessageListResponse> {\n return this.request<MessageListResponse>(\n 'GET',\n `/v1/agents/${agentId}/threads/${threadId}/messages`,\n );\n }\n\n /**\n * POST .../messages with stream forced on. Returns the raw Response so the\n * streaming service can consume the SSE body; only HTTP-level failures throw here.\n */\n async sendMessage(\n agentId: string,\n threadId: string,\n body: SendMessageBody,\n options?: { signal?: AbortSignal },\n ): Promise<Response> {\n const base = this.options.streamBaseUrl || this.options.baseUrl;\n const response = await fetch(`${base}/v1/agents/${agentId}/threads/${threadId}/messages`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Accept: 'text/event-stream',\n ...(await this.identityHeaders()),\n },\n body: JSON.stringify({ ...body, stream: true }),\n credentials: 'include',\n signal: options?.signal,\n });\n if (!response.ok) {\n throw await normalizeError(response);\n }\n return response;\n }\n\n /** POST .../stop. Needs the assistant message id captured from the stream's early events. */\n stopMessage(\n agentId: string,\n threadId: string,\n assistantMessageId: string,\n ): Promise<StopMessageResponse> {\n return this.request<StopMessageResponse>(\n 'POST',\n `/v1/agents/${agentId}/threads/${threadId}/stop`,\n { assistantMessageId },\n );\n }\n\n /** POST .../messages/:messageId/feedback. Idempotent; resending the same value is a no-op. */\n submitFeedback(\n agentId: string,\n threadId: string,\n messageId: string,\n feedback: 'POSITIVE' | 'NEGATIVE',\n ): Promise<FeedbackResponse> {\n return this.request<FeedbackResponse>(\n 'POST',\n `/v1/agents/${agentId}/threads/${threadId}/messages/${messageId}/feedback`,\n { feedback },\n );\n }\n\n /** Same endpoint as submitFeedback; omitting the feedback field clears it. */\n clearFeedback(agentId: string, threadId: string, messageId: string): Promise<FeedbackResponse> {\n return this.request<FeedbackResponse>(\n 'POST',\n `/v1/agents/${agentId}/threads/${threadId}/messages/${messageId}/feedback`,\n {},\n );\n }\n\n /** GET /v1/agents/:agentId/quota (read-only; does not consume quota). */\n getQuota(agentId: string): Promise<QuotaResponse> {\n return this.request<QuotaResponse>('GET', `/v1/agents/${agentId}/quota`);\n }\n\n /** POST /v1/analytics/events. Callers usually fire and forget; a lost event is acceptable. */\n trackEvent(body: TrackEventBody): Promise<TrackEventResponse> {\n return this.request<TrackEventResponse>('POST', '/v1/analytics/events', body);\n }\n\n /** Identity and auth headers Base Camp expects on every request. */\n private async identityHeaders(): Promise<Record<string, string>> {\n const { deployEnv, jobNumber, tenantHint, getAuthToken } = this.options;\n const token = await getAuthToken();\n const headers: Record<string, string> = {\n 'x-24g-deploy-env': deployEnv,\n 'x-24g-job-number': jobNumber,\n };\n if (tenantHint) {\n headers['x-tenant-hint'] = tenantHint;\n }\n if (token) {\n // Pass through tokens that already carry a scheme (e.g. system tokens).\n headers.Authorization = token.includes(' ') ? token : `Bearer ${token}`;\n }\n return headers;\n }\n\n private async request<T>(method: string, path: string, body?: unknown): Promise<T> {\n const response = await fetch(`${this.options.baseUrl}${path}`, {\n method,\n headers: {\n ...(body !== undefined ? { 'Content-Type': 'application/json' } : {}),\n ...(await this.identityHeaders()),\n },\n body: body !== undefined ? JSON.stringify(body) : undefined,\n credentials: 'include',\n });\n if (!response.ok) {\n throw await normalizeError(response);\n }\n return (await response.json()) as T;\n }\n}\n\n/**\n * Builds a BaseCampError from a failed response. Base Camp's error body is\n * { error: string, errorId? }; validation errors put a machine code in the\n * error field with a separate human-readable message field.\n */\nasync function normalizeError(response: Response): Promise<BaseCampError> {\n let message = `Base Camp request failed (${response.status})`;\n let code: string | undefined;\n try {\n const payload = (await response.json()) as { error?: unknown; message?: unknown };\n const errorField = typeof payload.error === 'string' ? payload.error : undefined;\n const messageField = typeof payload.message === 'string' ? payload.message : undefined;\n if (messageField) {\n message = messageField;\n code = errorField;\n } else if (errorField) {\n message = errorField;\n if (/^[A-Z0-9_]+$/.test(errorField)) {\n code = errorField;\n }\n }\n } catch {\n // Non-JSON error body; keep the status-based message.\n }\n return new BaseCampError(response.status, message, code);\n}\n","/**\n * Shared SSE (Server-Sent Events) streaming service for Base Camp chat.\n *\n * Centralises the fetch -> ReadableStream -> SSE line-parsing pipeline. Consumers\n * provide a small `eventHandler` that maps Base Camp's domain-specific events\n * (`message_id`, `assistant_message_id`, `token_usage`, `used_sources`) to their own\n * callbacks. Common events (`response_delta`, `thinking_*`, `text_complete`, `error`)\n * are handled by the built-in defaults.\n *\n * Base Camp streams SSE over the POST response body, so `EventSource` (GET-only)\n * cannot be used; the stream is parsed manually here.\n */\n\nexport interface StreamingCallbacks {\n /** Receives each text chunk of the streamed response. */\n onChunk: (chunk: string) => void;\n /** Called when the stream ends normally (`[DONE]` or reader exhausted). */\n onComplete: () => void;\n /** Called on any unrecoverable error (network, parse, HTTP status). */\n onError: (error: Error) => void;\n\n // --- optional, domain-specific ---\n onMetadata?: (metadata: {\n messageId?: string;\n sources?: unknown[];\n hasSourceContext?: boolean;\n }) => void;\n onThinkingStart?: () => void;\n onThinkingChunk?: (chunk: string) => void;\n onThinkingEnd?: () => void;\n onTextComplete?: () => void;\n onTokenUsage?: (percentOfCumulativeLimit: number) => void;\n /** Called with the message quota parsed from X-RateLimit-* response headers. */\n onRateLimit?: (info: RateLimitInfo) => void;\n}\n\n/** Message quota reported by Base Camp's X-RateLimit-* headers. */\nexport interface RateLimitInfo {\n limit: number;\n remaining: number;\n /** Epoch seconds of the next quota reset. */\n resetSeconds: number;\n}\n\n/** Thrown when Base Camp responds 429: the caller's message quota is spent. */\nexport class RateLimitError extends Error {\n limit: number;\n\n remaining: number;\n\n /** ISO timestamp of the next quota reset. */\n resetAt: string;\n\n constructor(body: { error?: string; limit?: number; remaining?: number; resetAt?: string }) {\n super(body.error || 'Message limit reached.');\n this.name = 'RateLimitError';\n this.limit = body.limit ?? 0;\n this.remaining = body.remaining ?? 0;\n this.resetAt = body.resetAt ?? '';\n }\n}\n\n/**\n * Return `true` when the handler consumed the event so the built-in default\n * handler is skipped. Return `false`/`undefined` to let the default run.\n */\nexport type SSEEventHandler = (\n eventType: string,\n data: unknown,\n callbacks: StreamingCallbacks,\n) => boolean | void;\n\nexport interface StreamSSEOptions {\n /** AbortSignal to cancel the request (e.g. a stop button). */\n signal?: AbortSignal;\n /**\n * Extra request headers merged over the base streaming headers. The app\n * boundary supplies deployment identity / tenant headers here; the kit\n * itself stays config-free.\n */\n headers?: Record<string, string>;\n}\n\n/** Headers shared by every Base Camp streaming request. */\nconst BASE_STREAMING_HEADERS: Record<string, string> = {\n 'Content-Type': 'application/json',\n Accept: 'text/event-stream',\n};\n\n/** Narrows an unknown SSE payload to a plain object for field access. */\nfunction asRecord(data: unknown): Record<string, unknown> {\n return typeof data === 'object' && data !== null ? (data as Record<string, unknown>) : {};\n}\n\n/** Handles the events that are identical across Base Camp consumers. */\nfunction handleDefaultEvent(\n eventType: string,\n data: unknown,\n callbacks: StreamingCallbacks,\n): boolean {\n const fields = asRecord(data);\n\n switch (eventType) {\n case 'response_delta':\n if (fields.text !== undefined && fields.text !== null) {\n callbacks.onChunk(String(fields.text));\n }\n return true;\n\n case 'text_complete':\n callbacks.onTextComplete?.();\n if (fields.hasSourceContext !== undefined) {\n callbacks.onMetadata?.({ hasSourceContext: Boolean(fields.hasSourceContext) });\n }\n return true;\n\n case 'thinking_start':\n callbacks.onThinkingStart?.();\n return true;\n\n case 'thinking_delta':\n if (fields.text) {\n callbacks.onThinkingChunk?.(String(fields.text));\n }\n return true;\n\n case 'thinking_end':\n callbacks.onThinkingEnd?.();\n return true;\n\n case 'error': {\n const message = 'message' in fields ? String(fields.message) : 'Unknown SSE error';\n callbacks.onError(new Error(message));\n return true;\n }\n\n default:\n return false;\n }\n}\n\n/** Fallback when JSON parsing fails inside the read loop. */\nfunction handleParseFallback(\n eventType: string,\n rawData: string,\n callbacks: StreamingCallbacks,\n): void {\n if (eventType === 'response_delta' || eventType === 'thinking_delta' || !eventType) {\n if (eventType === 'thinking_delta') {\n callbacks.onThinkingChunk?.(rawData);\n } else {\n callbacks.onChunk(rawData);\n }\n }\n}\n\n/** Fallback for data that wasn't matched by any named event. */\nfunction handleUnknownEvent(data: unknown, callbacks: StreamingCallbacks): void {\n const fields = asRecord(data);\n\n if (fields.type === 'sources' || fields.type === 'USED_SOURCES') {\n callbacks.onMetadata?.({ sources: fields.data as unknown[] });\n } else if (fields.text !== undefined && fields.text !== null) {\n callbacks.onChunk(String(fields.text));\n } else if (fields.content !== undefined && fields.content !== null) {\n callbacks.onChunk(String(fields.content));\n }\n}\n\n/** Extracts the event type from an SSE `event:` field. */\nfunction parseEventType(line: string): string {\n const eventType = line.slice(7).trim();\n if (!eventType) console.warn('Empty event type received, ignoring');\n return eventType || '';\n}\n\n/**\n * Parses JSON from an SSE `data:` field and dispatches through the handler chain:\n * consumer handler -> built-in defaults -> unknown fallback.\n * Returns `true` when the `[DONE]` signal is received (stream should stop).\n */\nfunction dispatchSSEData(\n jsonData: string,\n currentEvent: string,\n callbacks: StreamingCallbacks,\n eventHandler?: SSEEventHandler,\n): boolean {\n if (jsonData === '[DONE]') {\n callbacks.onComplete();\n return true;\n }\n\n try {\n const parsed = JSON.parse(jsonData);\n const handled = eventHandler?.(currentEvent, parsed, callbacks);\n\n if (!handled && !handleDefaultEvent(currentEvent, parsed, callbacks)) {\n handleUnknownEvent(parsed, callbacks);\n }\n } catch {\n // If the error event payload isn't valid JSON, onError would never fire via the\n // normal path - call it explicitly so callers aren't left waiting.\n if (currentEvent === 'error') {\n callbacks.onError(new Error(jsonData || 'Unknown SSE error'));\n } else {\n handleParseFallback(currentEvent, jsonData, callbacks);\n }\n }\n\n return currentEvent === 'error';\n}\n\n/**\n * Classifies a single SSE line and performs the appropriate action.\n * Mutates `state.currentEvent` when an `event:` field is encountered.\n * Returns `true` when the stream should terminate (`[DONE]` received).\n */\nfunction processSSELine(\n line: string,\n state: { currentEvent: string },\n callbacks: StreamingCallbacks,\n eventHandler?: SSEEventHandler,\n): boolean {\n const trimmedLine = line.trim();\n\n if (trimmedLine === '') return false;\n\n if (line.startsWith('event: ')) {\n state.currentEvent = parseEventType(line);\n return false;\n }\n\n if (line.startsWith('data: ')) {\n return dispatchSSEData(line.slice(6), state.currentEvent, callbacks, eventHandler);\n }\n\n if (trimmedLine.startsWith('id: ') || trimmedLine.startsWith('retry: ')) {\n return false;\n }\n\n console.warn('Unknown SSE line format:', line);\n return false;\n}\n\n/** Reads chunks from the stream, splits into lines, and dispatches each line. */\nasync function readSSEStream(\n reader: ReadableStreamDefaultReader<Uint8Array>,\n callbacks: StreamingCallbacks,\n eventHandler?: SSEEventHandler,\n): Promise<void> {\n const decoder = new TextDecoder();\n const state = { currentEvent: '', buffer: '' };\n\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n\n state.buffer += decoder.decode(value, { stream: true });\n\n const lines = state.buffer.split('\\n');\n state.buffer = lines.pop() || '';\n\n for (const line of lines) {\n if (processSSELine(line, state, callbacks, eventHandler)) return;\n }\n }\n\n // The stream can end without a trailing newline, leaving the final event's data line\n // sitting in the buffer. used_sources is the last event Base Camp sends, so dropping\n // this would silently lose every message's sources.\n state.buffer += decoder.decode();\n if (state.buffer.trim() !== '') {\n processSSELine(state.buffer, state, callbacks, eventHandler);\n }\n\n callbacks.onComplete();\n } finally {\n reader.releaseLock();\n }\n}\n\n/**\n * Opens an SSE connection via `fetch`, reads the stream, and dispatches events.\n * Cookies are sent (`credentials: 'include'`) so Base Camp can resolve the session user.\n *\n * @param url Full URL of the Base Camp streaming endpoint.\n * @param body JSON-serialisable request body.\n * @param callbacks Lifecycle callbacks the consumer cares about.\n * @param eventHandler Optional Base Camp event router; return `true` to skip defaults.\n * @param options Optional settings (AbortSignal, extra headers, etc.).\n */\n/**\n * Reads an already-made streaming response. Use this when something else,\n * like the Base Camp client, performed the request and handed back the raw\n * Response. Reports quota headers, then feeds every event to the callbacks.\n */\nexport async function consumeSSEResponse(\n response: Response,\n callbacks: StreamingCallbacks,\n eventHandler?: SSEEventHandler,\n): Promise<void> {\n try {\n // Base Camp reports the message quota on every send; 'unlimited' (or a\n // missing header when quota headers aren't exposed) means no limit UI.\n const limitHeader = response.headers.get('X-RateLimit-Limit');\n if (limitHeader && limitHeader !== 'unlimited' && callbacks.onRateLimit) {\n const limit = Number(limitHeader);\n const remaining = Number(response.headers.get('X-RateLimit-Remaining'));\n const resetSeconds = Number(response.headers.get('X-RateLimit-Reset'));\n if (Number.isFinite(limit) && Number.isFinite(remaining)) {\n callbacks.onRateLimit({ limit, remaining, resetSeconds });\n }\n }\n\n if (response.status === 429) {\n throw new RateLimitError(await response.json().catch(() => ({})));\n }\n\n if (!response.ok) {\n throw new Error(`HTTP error! status: ${response.status}`);\n }\n\n const reader = response.body?.getReader();\n if (!reader) {\n throw new Error('No response body reader available');\n }\n\n await readSSEStream(reader, callbacks, eventHandler);\n } catch (error) {\n // The user cancelled, so stay quiet. Checked by name because an abort is\n // not always an Error object, so `instanceof Error` can miss it.\n if ((error as { name?: string } | null)?.name === 'AbortError') return;\n\n console.error('SSE streaming error:', error);\n callbacks.onError(error instanceof Error ? error : new Error('Unknown streaming error'));\n }\n}\n\nexport async function streamSSE(\n url: string,\n body: unknown,\n callbacks: StreamingCallbacks,\n eventHandler?: SSEEventHandler,\n options?: StreamSSEOptions,\n): Promise<void> {\n try {\n const response = await fetch(url, {\n method: 'POST',\n headers: { ...BASE_STREAMING_HEADERS, ...options?.headers },\n body: JSON.stringify(body),\n credentials: 'include',\n signal: options?.signal,\n });\n\n await consumeSSEResponse(response, callbacks, eventHandler);\n } catch (error) {\n // The user cancelled, so stay quiet. Checked by name because an abort is\n // not always an Error object, so `instanceof Error` can miss it.\n if ((error as { name?: string } | null)?.name === 'AbortError') return;\n\n console.error('SSE streaming error:', error);\n callbacks.onError(error instanceof Error ? error : new Error('Unknown streaming error'));\n }\n}\n","/**\n * Renders LaTeX spans in an answer, for agents whose corpus contains formulas.\n *\n * Off unless an app asks for it (`useChatEngine({ showMath: true })`). MathLive is\n * an optional peer dependency loaded on first use, so an app that leaves math\n * off never installs it and never downloads it.\n *\n * Two parts, because markdown runs first and would otherwise eat the LaTeX.\n * `protectMathSource` is a marked rule that claims a `$...$` span before\n * markdown can touch what is inside it, and writes it back out as its own\n * source text. `renderMathIn` then finds those spans on the sanitized DOM and\n * replaces them with markup.\n *\n * Splitting it in two is what keeps MathLive's output away from the strict\n * allowlist in markdown.ts, which would strip the classes and inline styles\n * that output depends on.\n */\nimport DOMPurify from 'dompurify';\nimport type { RendererExtension, TokenizerExtension, Tokens } from 'marked';\n\nexport interface MathOptions {\n /**\n * Where MathLive's font files are served from, as a URL path.\n *\n * Most apps should leave this alone and `import 'mathlive/static.css'`\n * instead: that stylesheet declares the font faces itself, with URLs\n * relative to the package, so the bundler emits the font files. This option\n * is for serving them from somewhere else, such as a CDN or an unusual base\n * path, in which case skip the stylesheet import.\n */\n fontsDirectory?: string;\n}\n\n/** `true` for the defaults, or an object to point at the fonts. */\nexport type MathSetting = boolean | MathOptions;\n\nexport function mathOptionsOf(setting: MathSetting | undefined): MathOptions | null {\n if (!setting) return null;\n return setting === true ? {} : setting;\n}\n\n/** Loaded once, on the first answer that actually contains math. */\nlet converterPromise: Promise<((latex: string) => string) | null> | null = null;\n\n/**\n * The first answer with math decides the font location, since the library is\n * configured as it loads. One app does not change this between messages.\n */\nfunction loadConverter(options: MathOptions): Promise<((latex: string) => string) | null> {\n // KEEP THIS IMPORT DYNAMIC. Turning it into a top-level\n // `import { convertLatexToMarkup } from 'mathlive'` looks like tidying away\n // a lazy singleton, and costs every app using the kit an eager 800KB of\n // MathLive in its entry chunk, whether or not it renders formulas. As a\n // dynamic import it becomes a separate chunk that is only ever fetched by an\n // app that turns math on AND receives an answer containing a formula.\n // A test in __tests__/math.spec.ts fails if this regresses.\n converterPromise ??= import('mathlive')\n .then((mathlive) => {\n if (options.fontsDirectory !== undefined) {\n mathlive.MathfieldElement.fontsDirectory = options.fontsDirectory;\n }\n return mathlive.convertLatexToMarkup;\n })\n .catch(() => {\n // The peer dependency is optional, so a missing install is a plain\n // warning and formulas stay as written rather than breaking the answer.\n console.warn(\n '[chat-kit] math is on but mathlive is not installed. Run: pnpm add mathlive',\n );\n return null;\n });\n return converterPromise;\n}\n\n/** Longest inline span treated as a formula; see isFormula. */\nconst MAX_INLINE_LENGTH = 200;\n\n/** A character that only appears in LaTeX, never in a price. */\nconst LATEX_SIGNAL = /[\\\\^_{}]/;\n\n/**\n * Decides whether the text between two `$` really is a formula.\n *\n * `$` is also a currency symbol, and getting this wrong is worse in one\n * direction than the other: a missed formula still shows the reader its own\n * source text, while a misread price turns into markup garbage mid-sentence.\n * So all three rules below reject when unsure.\n */\nfunction isFormula(content: string, display: boolean): boolean {\n // `$$` is unambiguous; nobody writes a doubled dollar sign for money.\n if (display) return content.trim().length > 0;\n\n // A formula hugs its delimiters. This is what keeps \"costs $45 and up to\n // $90\" from reading as one span, since the content would end in a space.\n if (content.length === 0 || /^\\s|\\s$/.test(content)) return false;\n\n // Runaway spans: a stray `$` should not pair with another paragraphs away.\n if (content.length > MAX_INLINE_LENGTH) return false;\n\n // Either it looks like LaTeX, or it at least does not look like money.\n // This is what rejects \"$45-$90\" and \"$45 (was $60)\", whose content\n // (\"45-\" and \"45 (was \") carries no LaTeX character and starts with a digit.\n return LATEX_SIGNAL.test(content) || !/^\\d/.test(content);\n}\n\n/** Matches `$$...$$` first so a display span is never read as two inline ones. */\nconst SPAN = /\\$\\$([^$]+)\\$\\$|\\$([^$]+)\\$/g;\n\n/** The same shapes as SPAN, anchored, for asking \"does a span start here\". */\nconst SPAN_AT_START = /^\\$\\$([^$]+)\\$\\$|^\\$([^$]+)\\$/;\n\n/** Enough escaping for text content, which is all this rule ever emits. */\nfunction escapeHtmlText(text: string): string {\n return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');\n}\n\n/**\n * A marked rule that carries a math span through markdown unchanged.\n *\n * Markdown's own escape rule would otherwise get there first and strip the\n * backslash out of any escaped punctuation, so `$\\{x\\}$` reached MathLive as\n * `${x}$` and `$$\\left\\{x\\right\\}$$` as an invalid `\\left{`. Only backslashes\n * before punctuation were affected, which is why `\\frac` worked and made the\n * problem easy to miss.\n *\n * This claims the span and writes it straight back out, so the text reaching\n * the DOM is exactly what the model wrote. Rendering it is renderMathIn's job.\n */\nexport const protectMathSource: TokenizerExtension & RendererExtension = {\n name: 'mathSource',\n level: 'inline',\n // Lets marked skip ahead to the next candidate instead of trying every\n // character, and keeps it from cutting a span in half when it does.\n start: (src) => src.indexOf('$'),\n tokenizer(src) {\n const match = SPAN_AT_START.exec(src);\n if (!match) return undefined;\n\n const [raw, displayContent, inlineContent] = match;\n const content = displayContent ?? inlineContent ?? '';\n // Declining here hands the text back to markdown, which is what keeps\n // \"costs $45 and up to $90\" from being protected as a formula.\n if (!isFormula(content, displayContent !== undefined)) return undefined;\n\n return { type: 'mathSource', raw, text: raw };\n },\n renderer: (token) => escapeHtmlText((token as Tokens.Generic).text as string),\n};\n\n/**\n * MathLive builds its markup from the LaTeX it is given, and that LaTeX came\n * from the model. Sanitizing its output keeps a hostile formula from becoming\n * markup the main sanitizer never saw, at the cost of one extra pass.\n */\nfunction sanitizeMathMarkup(markup: string): string {\n return DOMPurify.sanitize(markup, {\n USE_PROFILES: { html: true, mathMl: true, svg: true },\n ADD_ATTR: ['class', 'style', 'aria-hidden'],\n });\n}\n\n/**\n * Replaces formulas inside `container` with rendered markup, in place.\n *\n * Text inside `code` and `pre` is left alone: a fenced block showing LaTeX\n * source is showing it deliberately.\n */\nexport async function renderMathIn(\n container: HTMLElement,\n options: MathOptions = {},\n): Promise<void> {\n const walker = container.ownerDocument.createTreeWalker(container, NodeFilter.SHOW_TEXT, {\n acceptNode: (node) =>\n node.parentElement?.closest('code, pre')\n ? NodeFilter.FILTER_REJECT\n : NodeFilter.FILTER_ACCEPT,\n });\n\n const candidates: Text[] = [];\n for (let node = walker.nextNode(); node; node = walker.nextNode()) {\n if (node.nodeValue?.includes('$')) candidates.push(node as Text);\n }\n\n // Loaded even for an answer with no formulas in it. An app that turned math\n // on without installing MathLive then hears about it on its first answer,\n // rather than whenever a formula first happens to come up: the wrong version\n // of that is a developer testing with plain questions, seeing no warning,\n // and concluding math works. The promise is cached, so this costs one import\n // for the life of the page and only for apps that asked for math.\n const convert = await loadConverter(options);\n if (!convert || candidates.length === 0) return;\n\n for (const node of candidates) {\n const source = node.nodeValue ?? '';\n // Built as a fragment so the surrounding text nodes stay untouched text.\n const fragment = container.ownerDocument.createDocumentFragment();\n let cursor = 0;\n let matched = false;\n\n for (const match of source.matchAll(SPAN)) {\n const [whole, displayContent, inlineContent] = match;\n const content = displayContent ?? inlineContent ?? '';\n const display = displayContent !== undefined;\n if (!isFormula(content, display)) continue;\n\n let markup: string;\n try {\n markup = convert(display ? `\\\\displaystyle ${content}` : content);\n } catch {\n // Invalid LaTeX: leave the span as the reader's own source text.\n continue;\n }\n\n matched = true;\n fragment.append(source.slice(cursor, match.index));\n const host = container.ownerDocument.createElement('span');\n host.className = display ? 'chat-math chat-math--display' : 'chat-math';\n host.innerHTML = sanitizeMathMarkup(markup);\n fragment.append(host);\n cursor = match.index + whole.length;\n }\n\n if (!matched) continue;\n fragment.append(source.slice(cursor));\n node.replaceWith(fragment);\n }\n}\n","/**\n * Sanitized markdown rendering for chat messages.\n *\n * marked parses, DOMPurify sanitizes against a strict allowlist. The sanitization\n * config below is security-critical: changes require review plus updates to the\n * XSS vectors covered in __tests__/markdown.spec.ts.\n */\nimport DOMPurify from 'dompurify';\nimport { Marked } from 'marked';\n\nimport { mathOptionsOf, protectMathSource, renderMathIn, type MathSetting } from './math';\n\n// markedjs config\nconst markedOptions = { gfm: true, breaks: true };\n\n/** The parser for an app without math. */\nconst plain = new Marked(markedOptions);\n\n/**\n * The same parser, plus the rule that protects LaTeX from markdown. Kept as a\n * second instance rather than registered on the first, so an app with math\n * off parses exactly as it did before any of this existed.\n */\nconst withMath = new Marked(markedOptions).use({ extensions: [protectMathSource] });\n\n// DOMPurify config\nconst purifyConfig = {\n ALLOWED_TAGS: [\n 'p',\n 'br',\n 'strong',\n 'b',\n 'em',\n 'i',\n 'code',\n 'pre',\n 'h1',\n 'h2',\n 'h3',\n 'h4',\n 'h5',\n 'h6',\n 'ul',\n 'ol',\n 'li',\n 'a',\n 'blockquote',\n 'span',\n 'table',\n 'thead',\n 'tbody',\n 'tr',\n 'td',\n 'th',\n 's',\n ],\n ALLOWED_ATTR: ['href', 'target', 'rel', 'title', 'type'],\n ALLOWED_URI_REGEXP: /^https?:\\/\\//,\n KEEP_CONTENT: true,\n RETURN_DOM: false,\n RETURN_DOM_FRAGMENT: false,\n RETURN_DOM_IMPORT: false,\n ADD_TAGS: [],\n ADD_ATTR: [],\n};\n\n/**\n * Post-processing hook applied to the sanitized DOM before it is serialized.\n * Extension point for product-specific link handling (e.g. rewriting internal\n * links); the default only hardens external links.\n */\nexport type SanitizedHtmlPostProcessor = (container: HTMLElement) => void;\n\n/** Adds security attributes to every link so they open safely in a new tab. */\nconst hardenLinks: SanitizedHtmlPostProcessor = (container) => {\n const links = container.querySelectorAll('a[href]');\n links.forEach((link) => {\n link.setAttribute('target', '_blank');\n link.setAttribute('rel', 'noopener noreferrer');\n });\n};\n\n/**\n * Safely render markdown text to HTML\n * Uses marked for parsing and DOMPurify for sanitization\n * Uses setTimeout to yield control and prevent main thread blocking\n *\n * @param text - The markdown text to render\n * @param maxLength - Maximum input length (default: 50000 chars)\n * @returns Promise<Sanitized HTML string>\n */\nexport interface RenderMarkdownOptions {\n /** Maximum input length before the text is truncated. */\n maxLength?: number;\n /**\n * Renders `$...$` and `$$...$$` as formulas. Needs the optional `mathlive`\n * peer dependency, loaded on first use. Off by default: `$` is also a\n * currency symbol, so this only belongs on for a corpus that has math.\n */\n showMath?: MathSetting;\n}\n\nexport async function renderMarkdownSafe(\n text: string,\n options: RenderMarkdownOptions = {},\n): Promise<string> {\n const { maxLength = 50000, showMath = false } = options;\n if (!text) return '';\n\n // validations: input is a string, and not exceeding max length\n if (typeof text !== 'string') return '';\n let processedText = text;\n if (text.length > maxLength) {\n console.warn(`Markdown input truncated: ${text.length} chars > ${maxLength} limit`);\n processedText = `${text.substring(0, maxLength)}...`;\n }\n\n try {\n // For large inputs, yield control to prevent blocking\n if (processedText.length > 5000) {\n await new Promise<void>((resolve) => {\n setTimeout(resolve, 0);\n });\n }\n\n // Parse markdown. With math on, the parser that shields `$...$` spans\n // from markdown's escape rule, so their backslashes are still there when\n // renderMathIn reads them below.\n const mathOptions = mathOptionsOf(showMath);\n const html = (mathOptions ? withMath : plain).parse(processedText);\n\n // ensure we have a string for sanitization - handle both sync and async cases\n const htmlString = typeof html === 'string' ? html : await Promise.resolve(html);\n\n const sanitized = DOMPurify.sanitize(htmlString, {\n ...purifyConfig,\n SANITIZE_DOM: true,\n WHOLE_DOCUMENT: false,\n RETURN_DOM: false,\n CUSTOM_ELEMENT_HANDLING: {\n tagNameCheck: null,\n attributeNameCheck: null,\n allowCustomizedBuiltInElements: false,\n },\n });\n\n // temporary div for post-processing\n const tempDiv = document.createElement('div');\n tempDiv.innerHTML = sanitized;\n\n hardenLinks(tempDiv);\n\n // After sanitizing, because the markup MathLive produces needs classes and\n // inline styles that the allowlist above deliberately strips. Its output\n // gets its own sanitize pass instead; see services/math.ts.\n if (mathOptions) {\n await renderMathIn(tempDiv, mathOptions);\n }\n\n return tempDiv.innerHTML;\n } catch (error) {\n console.error('Markdown rendering error:', error);\n // Fallback: use DOMPurify to escape text content safely and preserve line breaks\n return DOMPurify.sanitize(processedText.replace(/\\n/g, '<br>'), {\n ALLOWED_TAGS: ['br'],\n KEEP_CONTENT: true,\n });\n }\n}\n\n// ============================================================================\n// STREAMING MARKDOWN FUNCTIONALITY\n// ============================================================================\n\n/**\n * Result of streaming markdown rendering\n */\nexport interface StreamingRenderResult {\n html: string;\n isComplete: boolean;\n hasUnsafeBreak: boolean;\n lastSafePoint: number;\n}\n\n/**\n * streaming markdown renderer\n *\n * Render almost everything immediately, only avoid breaking mid-word\n * Uses async rendering to prevent main thread blocking\n */\nfunction findLastSafeBreakpoint(text: string): number {\n if (text.length === 0) {\n return 0;\n }\n\n // For very short content, just render it all\n if (text.length <= 10) {\n return text.length;\n }\n\n // Look for last space to avoid breaking words\n const lastSpace = text.lastIndexOf(' ');\n const lastNewline = text.lastIndexOf('\\n');\n const lastBreak = Math.max(lastSpace, lastNewline);\n\n // If we find a decent break point in the last 10 characters, use it\n // Otherwise just render everything - markdown libraries will handle it\n if (lastBreak >= text.length - 10) {\n return lastBreak + 1;\n }\n\n // If we reach here, it means we didn't find a good break point - just render everything\n return text.length;\n}\n\n/**\n * streaming markdown renderer\n *\n * @param text - The markdown text to render (potentially incomplete)\n * @param options - Rendering options\n * @returns Promise<StreamingRenderResult> with safe HTML and metadata\n */\nexport async function renderMarkdownStreaming(\n text: string,\n options: { forceComplete?: boolean } = {},\n): Promise<StreamingRenderResult> {\n if (!text) {\n return { html: '', isComplete: true, hasUnsafeBreak: false, lastSafePoint: 0 };\n }\n\n if (options.forceComplete) {\n const html = await renderMarkdownSafe(text);\n return {\n html,\n isComplete: true,\n hasUnsafeBreak: false,\n lastSafePoint: text.length,\n };\n }\n\n const lastSafePoint = findLastSafeBreakpoint(text);\n\n // Render up to the safe point\n const safeText = text.substring(0, lastSafePoint);\n const html = await renderMarkdownSafe(safeText);\n\n return {\n html,\n isComplete: lastSafePoint >= text.length,\n hasUnsafeBreak: lastSafePoint < text.length,\n lastSafePoint,\n };\n}\n\nexport default renderMarkdownSafe;\n","/**\n * Turns HTML character references back into the characters they stand for, so\n * a retrieved passage reading `&` displays as `&`.\n *\n * Retrieval text arrives with markup-special characters encoded, because the\n * documents behind it were HTML or were extracted through an HTML pipeline.\n * Answer text never shows the problem: it goes through the markdown renderer,\n * which decodes references on its way to HTML. Passages and source titles are\n * rendered as plain text instead, so without this they keep the raw `&`.\n *\n * SAFETY: the result is text, and must only ever be rendered as text (Vue's\n * `{{ }}`, `textContent`, or an escaped attribute). Decoding is not\n * sanitizing. `<script>` decodes to `<script>`, which is inert in a text\n * node and an injection if handed to `v-html` or `innerHTML`.\n */\n\n/**\n * The named references worth handling. Everything markup-critical is here,\n * plus the non-breaking space that PDF extraction leaves behind. HTML defines\n * around two thousand more; those arrive from real pipelines in numeric form,\n * which is handled generically below.\n */\nconst NAMED: Record<string, string> = {\n amp: '&',\n lt: '<',\n gt: '>',\n quot: '\"',\n apos: \"'\",\n nbsp: '\\u00a0',\n};\n\n// Decimal (&), hexadecimal (&), or a name (&).\nconst REFERENCE = /&(?:#(\\d{1,7})|#[xX]([0-9a-fA-F]{1,6})|([a-zA-Z][a-zA-Z0-9]{1,31}));/g;\n\nfunction fromCodePoint(value: number): string | null {\n // Surrogate halves are not characters on their own, and fromCodePoint\n // throws above the Unicode range.\n if (!Number.isInteger(value) || value < 0 || value > 0x10ffff) return null;\n if (value >= 0xd800 && value <= 0xdfff) return null;\n return String.fromCodePoint(value);\n}\n\n/**\n * Drops HTML tags from retrieved text, keeping only what a reader would see.\n * Extraction pipelines leave passage text with fragments of source markup in\n * it, and a passage is rendered as plain text, where \"<p>\" would read as\n * literal noise. Strip before decoding entities, so an author's own \"<p>\"\n * survives as the \"<p>\" they wrote instead of being taken for a tag.\n */\nexport function stripHtmlTags(text: string): string {\n return text.replace(/<[^>]*>/g, '');\n}\n\n/**\n * Decodes character references in `text`. Runs once, not until stable, so\n * text that genuinely reads `&#x26;` decodes to `&` and stops, the\n * way a browser would. An unrecognized name is left exactly as it was.\n */\nexport function decodeHtmlEntities(text: string): string {\n // The overwhelmingly common case: nothing to do, and no regex run.\n if (!text.includes('&')) return text;\n\n return text.replace(REFERENCE, (reference, decimal, hex, name) => {\n if (decimal !== undefined) return fromCodePoint(Number(decimal)) ?? reference;\n if (hex !== undefined) return fromCodePoint(Number.parseInt(hex, 16)) ?? reference;\n return NAMED[(name as string).toLowerCase()] ?? reference;\n });\n}\n","import { renderMarkdownSafe } from './markdown';\nimport type { MathSetting } from './math';\n\n/**\n * Manages streaming markdown rendering for a single message. Collects text as\n * it arrives and re-renders it on a throttle, so the markdown parse runs at\n * most a few dozen times a second no matter how fast the text comes in.\n * Framework-free; the consumer wires `onUpdate` into its own reactive state.\n */\nexport default class StreamingMessage {\n private buffer: string = '';\n\n private renderTimer: ReturnType<typeof setTimeout> | null = null;\n\n private destroyed: boolean = false;\n\n private stopped: boolean = false;\n\n constructor(\n private onUpdate: (html: string, isComplete: boolean) => void,\n private throttleMs: number = 25, // up to 40 renders a second\n /** Renders formulas on every pass, so they typeset as the answer arrives. */\n private showMath: MathSetting = false,\n ) {}\n\n addChunk(chunk: string): void {\n if (this.destroyed || this.stopped) return;\n\n this.buffer += chunk;\n\n // A throttle, not a debounce: resetting the timer on every chunk would\n // starve rendering whenever text arrives faster than the delay.\n if (!this.renderTimer) {\n this.renderTimer = setTimeout(() => {\n this.renderTimer = null;\n void this.render(false);\n }, this.throttleMs);\n }\n }\n\n /** Renders everything received so far and marks the message finished. */\n async complete(): Promise<void> {\n if (this.destroyed || this.stopped) return;\n\n if (this.renderTimer) {\n clearTimeout(this.renderTimer);\n this.renderTimer = null;\n }\n\n await this.render(true);\n }\n\n private async render(isComplete: boolean): Promise<void> {\n if (this.destroyed || this.stopped) return;\n\n try {\n // Math renders on every pass, so a formula is typeset by the time the\n // reader reaches it rather than appearing as source and then changing.\n // A half-arrived `$\\frac{a` has no closing delimiter yet, so it is not\n // matched at all and simply stays as text until the rest of it lands.\n const html = await renderMarkdownSafe(this.buffer, { showMath: this.showMath });\n // Rendering is async, so a stop or destroy can land while it runs.\n if (this.destroyed || this.stopped) return;\n this.onUpdate(html, isComplete);\n } catch (error) {\n console.error('Error rendering streaming markdown:', error);\n }\n }\n\n /** The raw markdown received so far. */\n getCurrentContent(): string {\n return this.buffer;\n }\n\n /**\n * Stops further renders. Whatever is already on screen stays there, so a\n * partial answer is never blanked; the caller owns the final render.\n */\n stop(): void {\n if (this.destroyed || this.stopped) return;\n\n this.stopped = true;\n\n if (this.renderTimer) {\n clearTimeout(this.renderTimer);\n this.renderTimer = null;\n }\n }\n\n destroy(): void {\n this.destroyed = true;\n\n if (this.renderTimer) {\n clearTimeout(this.renderTimer);\n this.renderTimer = null;\n }\n }\n}\n","/**\n * Paces streamed text so it reads as steady typing.\n *\n * Base Camp's stream often arrives in bursts: nothing for a while, then a\n * large block at once. Writing a burst straight to the screen makes the\n * answer lurch. This sits between the network and the message, queueing\n * whatever arrives and letting it out a few characters per frame.\n *\n * The pace is constant: the same characters per frame from the first word to\n * the last, so the reveal never visibly accelerates. Hidden tabs skip the\n * animation entirely, because browsers suspend frame callbacks there and the\n * backlog would replay on return.\n */\n\n/**\n * Characters per frame at speed 1: about 240 a second on a 60Hz display.\n *\n * Chosen to sit just ahead of how fast a model generates, so the queue stays\n * near-empty and the reveal tracks the stream instead of falling behind it.\n * An earlier version started at one character per frame and accelerated to\n * catch up, which is what made answers appear to speed up as they went.\n */\nconst BASE_CHARS_PER_FRAME = 4;\n\n/**\n * How far the reveal may trail the stream before the excess is shown at once.\n *\n * Only reached when text arrives far faster than any pace could type it: a\n * cached answer, a local backend, a reconnect that delivers everything in one\n * go. Without this the reveal would keep typing long after the answer landed,\n * and `finish()` would hold the interface in its streaming state throughout.\n */\nconst MAX_LAG_CHARS = 600;\n\nexport interface TypewriterOptions {\n /**\n * Pace multiplier. 1 is the kit's default feel, 2 types twice as fast, 0.5\n * at half. It scales the whole behavior, trickle and burst-drain alike, so\n * the reveal keeps its character at any setting. A value that makes no pace\n * (zero, negative, not a number) falls back to 1.\n */\n speed?: number;\n}\n\n/** `false` shows text the instant it arrives; `true` or options turn pacing on. */\nexport type TypewriterSetting = boolean | TypewriterOptions;\n\nexport default class Typewriter {\n private queue = '';\n\n /** Characters revealed per frame, fixed for the life of the reveal. */\n private readonly charsPerFrame: number;\n\n /**\n * Budget left over from earlier frames. Fractional speeds need it: at 0.5\n * the budget is half a character per frame, which becomes one character\n * every other frame instead of none at all.\n */\n private carry = 0;\n\n private readonly enabled: boolean;\n\n private frameHandle: number | null = null;\n\n private waiters: (() => void)[] = [];\n\n private destroyed = false;\n\n private readonly onVisibilityChange = () => {\n if (document.hidden) this.flush();\n };\n\n constructor(\n private emit: (text: string) => void,\n setting: TypewriterSetting = true,\n ) {\n this.enabled = setting !== false;\n const speed = typeof setting === 'object' ? setting.speed : undefined;\n const multiplier =\n typeof speed === 'number' && Number.isFinite(speed) && speed > 0 ? speed : 1;\n this.charsPerFrame = BASE_CHARS_PER_FRAME * multiplier;\n if (this.enabled) {\n document.addEventListener('visibilitychange', this.onVisibilityChange);\n }\n }\n\n /** Queues text for the reveal. Emits right away when pacing is off or the tab is hidden. */\n push(text: string): void {\n if (this.destroyed || !text) return;\n\n this.queue += text;\n\n if (!this.enabled || document.hidden) {\n this.flush();\n return;\n }\n\n this.schedule();\n }\n\n /** Emits everything still queued, immediately and synchronously. */\n flush(): void {\n if (this.destroyed) return;\n this.cancelFrame();\n this.carry = 0;\n if (this.queue) {\n const rest = this.queue;\n this.queue = '';\n this.emit(rest);\n }\n this.resolveWaiters();\n }\n\n /** Resolves once the queue has drained at its own pace. */\n finish(): Promise<void> {\n if (this.destroyed || !this.queue) return Promise.resolve();\n // A hidden tab gets no frames, so a paced drain would never end.\n if (document.hidden) {\n this.flush();\n return Promise.resolve();\n }\n return new Promise((resolve) => {\n this.waiters.push(resolve);\n });\n }\n\n destroy(): void {\n if (this.destroyed) return;\n this.destroyed = true;\n this.cancelFrame();\n this.queue = '';\n document.removeEventListener('visibilitychange', this.onVisibilityChange);\n this.resolveWaiters();\n }\n\n private schedule(): void {\n if (this.frameHandle !== null) return;\n const tick = () => {\n this.frameHandle = null;\n // A frame can still fire after destroy or flush already emptied the queue.\n if (this.destroyed || !this.queue) return;\n this.step();\n if (this.queue) this.schedule();\n else this.resolveWaiters();\n };\n this.frameHandle = requestAnimationFrame(tick);\n }\n\n /** Emits one frame's worth of characters. */\n private step(): void {\n // The carry banks what a frame does not spend, so a speed below one\n // character per frame still advances, just on later frames.\n this.carry += this.charsPerFrame;\n\n // Text arriving faster than any pace can type it: show everything beyond\n // the allowed lag at once, then carry on typing the tail uniformly.\n const overflow = Math.max(0, this.queue.length - MAX_LAG_CHARS);\n\n let take = Math.min(overflow + Math.floor(this.carry), this.queue.length);\n // A sub-one budget waits for a later frame to afford its character.\n if (take < 1) return;\n this.carry -= take - overflow;\n // Never split a surrogate pair, so emoji come out whole. The second half\n // rides free rather than charging the budget, matching the pace the kit\n // always had at the default speed.\n const lastCode = this.queue.charCodeAt(take - 1);\n if (lastCode >= 0xd800 && lastCode <= 0xdbff && take < this.queue.length) take += 1;\n const out = this.queue.slice(0, take);\n this.queue = this.queue.slice(take);\n if (!this.queue) this.carry = 0;\n this.emit(out);\n }\n\n private cancelFrame(): void {\n if (this.frameHandle === null) return;\n cancelAnimationFrame(this.frameHandle);\n this.frameHandle = null;\n }\n\n private resolveWaiters(): void {\n for (const resolve of this.waiters.splice(0)) resolve();\n }\n}\n","/**\n * The chat's brain. Holds the conversation state and talks to Base Camp, so\n * the components only ever render what they find here and call the actions.\n *\n * The app creates one engine and hands it to the Chat component. Because the app\n * owns the engine, it can also drive the chat from outside the component,\n * for example sending a suggested question or starting a new conversation\n * from the top bar.\n */\nimport { computed, reactive, readonly, ref, shallowRef } from 'vue';\n\nimport { consumeSSEResponse } from '../services/streaming.service';\nimport StreamingMessage from '../services/streaming-message';\nimport Typewriter, { type TypewriterSetting } from '../services/typewriter';\nimport { renderMarkdownSafe } from '../services/markdown';\nimport type { MathSetting } from '../services/math';\nimport type {\n AgentDetails,\n ChatCitation,\n ChatEvent,\n ChatInputMode,\n ChatMessage,\n CreateThreadBody,\n CreateThreadResponse,\n FeedbackResponse,\n MessageFeedback,\n MessageListResponse,\n QuotaResponse,\n SendMessageBody,\n StopMessageResponse,\n Thread,\n TokenUsage,\n UsedSource,\n} from '../types';\n\n/**\n * What the engine needs from a chat backend. The real Base Camp client\n * satisfies this, and so does the mock used for tests and local development.\n */\nexport interface ChatTransport {\n getAgent(agentId: string): Promise<AgentDetails>;\n createThread(agentId: string, body?: CreateThreadBody): Promise<CreateThreadResponse>;\n sendMessage(\n agentId: string,\n threadId: string,\n body: SendMessageBody,\n options?: { signal?: AbortSignal },\n ): Promise<Response>;\n stopMessage(\n agentId: string,\n threadId: string,\n assistantMessageId: string,\n ): Promise<StopMessageResponse>;\n submitFeedback(\n agentId: string,\n threadId: string,\n messageId: string,\n feedback: MessageFeedback,\n ): Promise<FeedbackResponse>;\n clearFeedback(agentId: string, threadId: string, messageId: string): Promise<FeedbackResponse>;\n /**\n * The two history methods are optional so a minimal transport stays easy to\n * write. The real Base Camp client implements both; the engine's listThreads\n * and loadThread refuse clearly when a transport leaves them out.\n *\n * getThreads returns at most `limit` threads, newest first, in whatever\n * status they are in; the engine does the filtering.\n */\n getThreads?(agentId: string, limit: number): Promise<Thread[]>;\n getMessages?(agentId: string, threadId: string): Promise<MessageListResponse>;\n /**\n * The sender's remaining message allowance. Optional like the history\n * methods, and read on the engine's own initiative rather than on request,\n * so a transport without it leaves `quota` null instead of refusing.\n */\n getQuota?(agentId: string): Promise<QuotaResponse>;\n}\n\n/** One message as the components render it. */\nexport interface ChatEngineMessage {\n /** Local id, stable for the life of the conversation. */\n id: string;\n role: 'user' | 'assistant';\n /** The raw text. For assistant messages it grows while streaming. */\n text: string;\n /** Rendered, sanitized HTML for assistant messages. */\n html: string;\n /** The model's reasoning for this reply; grows while it streams. Empty for user messages. */\n thinking: string;\n basecampMessageId?: string;\n sources: UsedSource[];\n /** Passage-level inline citations; stays empty for prose-mode agents. */\n citations: ChatCitation[];\n feedback: MessageFeedback | null;\n isOutOfScope: boolean;\n isStreaming: boolean;\n /**\n * True while the server is still sending this reply.\n *\n * Goes false when the stream closes, which is when citations land and their\n * popovers start working. That is earlier than `isStreaming`, because the\n * reveal carries on typing out text that has already arrived, so this is\n * the one to watch for \"is the server still working\".\n */\n isReceiving: boolean;\n failed: boolean;\n /** How the user entered this message (typed or voice); only user messages set it. */\n inputMode?: ChatInputMode;\n}\n\n/**\n * What the engine needs to exist at all: which agent, reached how. Not\n * settings, and there are no defaults to fall back on.\n */\nexport interface ChatEngineConnection {\n transport: ChatTransport;\n agentId: string;\n}\n\n/**\n * Everything an app may adjust. All optional, and every default is the\n * behaviour the kit had before the setting existed, so an engine built from\n * the connection alone is a working chat.\n */\nexport interface ChatEngineSettings {\n /**\n * Reveals answers a few characters at a time instead of in raw network\n * bursts. On unless explicitly turned off. Pass { speed } to retune the\n * pace for an app: 2 types twice as fast as the default, 0.5 at half.\n */\n typewriter?: TypewriterSetting;\n /**\n * Decides whether a finished answer is the assistant declining a question it\n * cannot help with. Those replies get the out-of-scope marking and no thumbs\n * rating. Apps that recognise a set decline message match against it here.\n */\n isOutOfScope?: (text: string) => boolean;\n /**\n * Renders `$...$` and `$$...$$` in answers as formulas. Needs the optional\n * `mathlive` peer dependency (`pnpm add mathlive`), which is loaded on the\n * first answer that contains any. Leave off unless the corpus has math: `$`\n * is also a currency symbol, and prices should stay prices.\n *\n * `true` uses MathLive's defaults; pass `{ fontsDirectory }` to say where\n * its font files are served from.\n */\n showMath?: MathSetting;\n /**\n * Called when something the app may want to record happens: a turn\n * finishes, a thumbs rating is saved, a citation is opened. Listening\n * never changes how the chat behaves, even if the listener throws.\n */\n onEvent?: (event: ChatEvent) => void;\n}\n\n/** The two halves together, as useChatEngine takes them. */\nexport type ChatEngineOptions = ChatEngineConnection & ChatEngineSettings;\n\n/** One page of a history rail, from listThreads. */\nexport interface ThreadPage {\n /** Open threads only, newest first. */\n threads: Thread[];\n /** True when the page came back full, so asking for more may find more. */\n mightHaveMore: boolean;\n}\n\n/** How many threads listThreads asks for when the app does not say. */\nconst DEFAULT_THREAD_PAGE = 20;\n\nexport type ChatStatus = 'idle' | 'thinking' | 'streaming';\n\nlet nextLocalId = 0;\nfunction localId(): string {\n nextLocalId += 1;\n return `local-${nextLocalId}`;\n}\n\nexport function useChatEngine(options: ChatEngineOptions) {\n const { transport, agentId } = options;\n\n const agent = shallowRef<AgentDetails | null>(null);\n const threadId = ref<string | null>(null);\n const messages = ref<ChatEngineMessage[]>([]);\n const status = ref<ChatStatus>('idle');\n const error = ref<Error | null>(null);\n\n /** Thread-level token accounting, updated by the stream's token_usage event. */\n const tokenUsage = shallowRef<TokenUsage | null>(null);\n\n /**\n * The sender's remaining message allowance, or null when the transport\n * cannot report one. Read at startup and again after each finished turn,\n * which is when the number moves; the endpoint does not consume quota\n * itself, so asking is free.\n */\n const quota = shallowRef<QuotaResponse | null>(null);\n\n /**\n * Kept fresh on the engine's own initiative, because nothing asked for it.\n * A failure therefore leaves the last known value alone and stays quiet: an\n * allowance the interface may not even display is no reason to interrupt a\n * conversation that is working.\n */\n async function refreshQuota(): Promise<void> {\n if (!transport.getQuota) return;\n try {\n quota.value = await transport.getQuota(agentId);\n } catch {\n // Left as it was; the next turn tries again.\n }\n }\n\n // The reply being generated right now. It only joins messages once its\n // first answer text arrives, but its thinking starts earlier and the\n // interface may want to show that while the user is still waiting.\n const pendingReply = shallowRef<ChatEngineMessage | null>(null);\n\n /** Reasoning streamed for the answer still being retrieved; '' once the answer starts. */\n const liveThinking = computed(() =>\n status.value === 'thinking' ? (pendingReply.value?.thinking ?? '') : '',\n );\n\n /**\n * What kind of failure `error` holds, so components can pick the right\n * wording without inspecting error classes. Rate limits are recognized by\n * shape (a 429 status or the streaming service's RateLimitError), which\n * also covers transports that throw their own error types.\n */\n const errorKind = computed<'rate-limited' | 'error' | null>(() => {\n const failure = error.value;\n if (!failure) return null;\n const status429 = (failure as { status?: number }).status === 429;\n return status429 || failure.name === 'RateLimitError' ? 'rate-limited' : 'error';\n });\n\n let abortController: AbortController | null = null;\n // Tracked so stopping or starting a new chat can wait for it to finish.\n let activeSend: Promise<void> | null = null;\n\n // The agent's settings arrive in the background. Nothing blocks on them,\n // but they carry the server-side switches like whether sources are shown.\n const agentReady = transport\n .getAgent(agentId)\n .then((details) => {\n agent.value = details;\n return details;\n })\n .catch(() => null);\n\n // So an interface that shows the allowance has it before the first question.\n void refreshQuota();\n\n const busy = computed(() => status.value !== 'idle');\n\n function findMessage(id: string): ChatEngineMessage | undefined {\n return messages.value.find((m) => m.id === id);\n }\n\n function emitEvent(event: ChatEvent): void {\n try {\n options.onEvent?.(event);\n } catch {\n // A listener problem must never break the conversation.\n }\n }\n\n /** The question a reply answered: the closest user message above it. */\n function turnContext(reply: ChatEngineMessage): { query: string; response: string } {\n const index = messages.value.indexOf(reply);\n const question = messages.value\n .slice(0, index)\n .reverse()\n .find((m) => m.role === 'user');\n return { query: question?.text ?? '', response: reply.text };\n }\n\n async function ensureThread(): Promise<string> {\n if (threadId.value) return threadId.value;\n const created = await transport.createThread(agentId);\n threadId.value = created.id;\n return created.id;\n }\n\n function sendMessage(text: string, inputMode: ChatInputMode = 'typed'): Promise<void> {\n const trimmed = text.trim();\n // Nothing sends while empty or while an answer is still coming in.\n if (!trimmed || busy.value) return Promise.resolve();\n\n const send = runSend(trimmed, inputMode);\n activeSend = send;\n void send.finally(() => {\n // Only clear if a newer send has not already claimed the slot.\n if (activeSend === send) activeSend = null;\n });\n return send;\n }\n\n async function runSend(trimmed: string, inputMode: ChatInputMode): Promise<void> {\n error.value = null;\n status.value = 'thinking';\n\n const userMessage = reactive<ChatEngineMessage>({\n id: localId(),\n role: 'user',\n text: trimmed,\n html: '',\n thinking: '',\n sources: [],\n citations: [],\n feedback: null,\n isOutOfScope: false,\n isStreaming: false,\n isReceiving: false,\n failed: false,\n inputMode,\n });\n messages.value.push(userMessage);\n\n const reply = reactive<ChatEngineMessage>({\n id: localId(),\n role: 'assistant',\n text: '',\n html: '',\n thinking: '',\n sources: [],\n citations: [],\n feedback: null,\n isOutOfScope: false,\n isStreaming: false,\n isReceiving: false,\n failed: false,\n });\n pendingReply.value = reply;\n\n const controller = new AbortController();\n abortController = controller;\n\n const renderer = new StreamingMessage(\n (html) => {\n reply.html = html;\n },\n 25,\n options.showMath,\n );\n\n // The reply bubble appears once the first piece of the answer arrives.\n // Until then the thinking indicator covers the wait.\n function beginStreaming() {\n if (status.value !== 'streaming') {\n status.value = 'streaming';\n reply.isStreaming = true;\n reply.isReceiving = true;\n messages.value.push(reply);\n }\n }\n\n // Paces the reveal so bursts from the network still read as steady typing.\n const typer = new Typewriter((text) => {\n beginStreaming();\n reply.text += text;\n renderer.addChunk(text);\n }, options.typewriter ?? true);\n\n // Failures inside the stream get collected here and thrown afterwards,\n // so all the cleanup lives in one catch block below.\n let streamError: Error | null = null;\n\n try {\n const thread = await ensureThread();\n const response = await transport.sendMessage(\n agentId,\n thread,\n {\n message: trimmed,\n stream: true,\n // The kit's mic dictates into the composer, so its 'voice' is the\n // API's 'dictation' and never 'realtime', which is a voice-to-voice\n // turn. Typed messages say nothing and take the server's 'keyboard'\n // default, which also keeps this working against a Base Camp that\n // predates the field.\n ...(inputMode === 'voice' && { inputMode: 'dictation' as const }),\n },\n { signal: controller.signal },\n );\n\n await consumeSSEResponse(\n response,\n {\n onChunk: (chunk) => {\n typer.push(chunk);\n },\n onComplete: () => {\n // The final render happens below, once the stream is fully read.\n },\n onError: (err) => {\n streamError = err;\n },\n onMetadata: (metadata) => {\n if (metadata.sources) {\n reply.sources = metadata.sources as UsedSource[];\n }\n },\n // Reasoning arrives before the answer text. It accumulates on the\n // reply itself, so it stays with the message after the turn; the\n // liveThinking ref shows it while the reply bubble does not exist yet.\n onThinkingChunk: (chunk) => {\n reply.thinking += chunk;\n },\n },\n (eventType, data) => {\n // Arrives after text_complete, with the whole payload as one array.\n if (eventType === 'citations' && Array.isArray(data)) {\n reply.citations = data as ChatCitation[];\n return true;\n }\n // Both id events carry { messageId }; see StreamMessageIdPayload.\n const fields = (data ?? {}) as Record<string, unknown>;\n if (eventType === 'message_id' && typeof fields.messageId === 'string') {\n userMessage.basecampMessageId = fields.messageId;\n return true;\n }\n if (eventType === 'assistant_message_id' && typeof fields.messageId === 'string') {\n reply.basecampMessageId = fields.messageId;\n return true;\n }\n // The payload is the source array itself, same as the citations\n // event; see StreamUsedSourcesPayload and Base Camp's own consumer.\n if (eventType === 'used_sources' && Array.isArray(data)) {\n reply.sources = data as UsedSource[];\n return true;\n }\n // Thread-level accounting rides along with every answer; see\n // StreamTokenUsagePayload for the full event shape.\n if (eventType === 'token_usage' && fields.thread) {\n tokenUsage.value = fields.thread as TokenUsage;\n return true;\n }\n return false;\n },\n );\n\n if (streamError) throw streamError;\n\n // Aborting ends the stream without throwing, so the signal is the only\n // way to tell a deliberate stop from an answer that finished.\n if (controller.signal.aborted) {\n typer.flush();\n await renderer.complete();\n reply.isReceiving = false;\n reply.isStreaming = false;\n return;\n }\n\n // The server has said everything it is going to, so anything watching\n // for it to finish can stop now; only the reveal is still running.\n reply.isReceiving = false;\n\n // Let the reveal finish playing out whatever has already arrived.\n await typer.finish();\n\n // An answer that produced no text at all counts as a failure.\n if (!reply.text) {\n throw new Error('The response ended without any content.');\n }\n\n await renderer.complete();\n reply.isOutOfScope = options.isOutOfScope?.(reply.text) ?? false;\n reply.isStreaming = false;\n emitEvent({\n type: 'turn',\n query: trimmed,\n inputMode,\n response: reply.text,\n isOutOfScope: reply.isOutOfScope,\n citations: reply.citations,\n sources: reply.sources,\n threadId: threadId.value,\n userMessageId: userMessage.basecampMessageId,\n assistantMessageId: reply.basecampMessageId,\n });\n } catch (err) {\n // Show everything that arrived, even what the reveal had not caught up to.\n typer.flush();\n // An abort thrown before the stream opened is a cancel, not a failure.\n const wasCancelled = (err as { name?: string } | null)?.name === 'AbortError';\n if (!wasCancelled) {\n if (reply.text) {\n reply.failed = true;\n } else {\n error.value = err instanceof Error ? err : new Error(String(err));\n }\n }\n renderer.stop();\n reply.isReceiving = false;\n reply.isStreaming = false;\n if (reply.text) {\n reply.html = await renderMarkdownSafe(reply.text, { showMath: options.showMath });\n }\n } finally {\n typer.destroy();\n abortController = null;\n pendingReply.value = null;\n status.value = 'idle';\n // In the finally, so a turn that failed or was stopped still refreshes:\n // a rejected send may well be the one that spent the last message.\n void refreshQuota();\n }\n }\n\n /**\n * Stops the answer being generated, keeping the text that already arrived.\n * Resolves once the send has finished, so callers can safely clear or\n * replace the conversation afterwards.\n */\n async function stopGeneration(): Promise<void> {\n const pending = activeSend;\n if (!pending) return;\n\n // Read before aborting, because the send clears isStreaming on its way out.\n const replyId = messages.value.find((m) => m.isStreaming)?.basecampMessageId;\n abortController?.abort();\n\n // Tell the server too, so it stops paying for tokens nobody will read.\n // Not awaited, so the interface never waits on this round trip.\n if (replyId && threadId.value) {\n void transport.stopMessage(agentId, threadId.value, replyId).catch(() => {});\n }\n\n await pending;\n }\n\n /** Clears the conversation and returns to the welcome state. */\n async function newConversation(): Promise<void> {\n // Waits for any send still running, so it cannot write to the\n // conversation we are about to clear.\n await stopGeneration();\n threadId.value = null;\n messages.value = [];\n error.value = null;\n }\n\n /**\n * Maps a stored message onto the shape the components render. Citation\n * markers are stripped from the text: passage-level citations are not\n * persisted with the message, so a marker could never become a chip again\n * and would only read as stray \"[^12]\" fragments.\n */\n async function fromHistory(stored: ChatMessage): Promise<ChatEngineMessage> {\n const isAssistant = stored.role === 'assistant';\n const text = isAssistant ? stored.text.replace(/\\[\\^\\d+\\]/g, '') : stored.text;\n return reactive<ChatEngineMessage>({\n id: localId(),\n role: stored.role,\n text,\n html: isAssistant ? await renderMarkdownSafe(text, { showMath: options.showMath }) : '',\n thinking: stored.metadata?.thinking ?? '',\n basecampMessageId: stored.id,\n sources: stored.sourceContext ?? [],\n citations: [],\n feedback: stored.feedback ?? null,\n // Re-derived on load, so a declined answer keeps its marking after a reload.\n isOutOfScope: isAssistant ? (options.isOutOfScope?.(stored.text) ?? false) : false,\n isStreaming: false,\n isReceiving: false,\n failed: false,\n });\n }\n\n /**\n * The current user's open threads for this agent, newest first, for a\n * history rail. Deleted and archived threads are left out here rather than\n * by the server, which returns every status.\n *\n * Ask for a bigger `limit` to page: `mightHaveMore` is true when the server\n * filled the page, since that is the only sign there could be another.\n */\n async function listThreads(limit = DEFAULT_THREAD_PAGE): Promise<ThreadPage> {\n if (!transport.getThreads) {\n throw new Error('This transport does not implement getThreads, so history cannot be listed.');\n }\n const fetched = await transport.getThreads(agentId, limit);\n return {\n threads: fetched.filter((thread) => thread.status === 'ACTIVE'),\n // Counted before filtering: the server applied the limit to all statuses.\n mightHaveMore: fetched.length >= limit,\n };\n }\n\n /**\n * Replaces the conversation with a stored thread, so past conversations can\n * be reopened. Waits for any in-flight send first, like newConversation.\n */\n async function loadThread(id: string): Promise<void> {\n if (!transport.getMessages) {\n throw new Error('This transport does not implement getMessages, so history cannot be loaded.');\n }\n await stopGeneration();\n const history = await transport.getMessages(agentId, id);\n // The thread-opening welcome text is the app's to render (its welcome\n // slot or copy), so the server's stored copy of it would show twice.\n const stored = history.messages.filter((m) => !m.metadata?.isWelcomeMessage);\n const loaded = await Promise.all(stored.map((m) => fromHistory(m)));\n threadId.value = id;\n messages.value = loaded;\n tokenUsage.value = history.tokenUsage ?? null;\n error.value = null;\n }\n\n /**\n * Records a thumbs rating. Tapping the same thumb again removes it. The\n * change shows immediately and rolls back if the server rejects it.\n */\n async function setFeedback(messageId: string, value: MessageFeedback): Promise<void> {\n const message = findMessage(messageId);\n if (!message?.basecampMessageId || !threadId.value) return;\n\n const previous = message.feedback;\n const next = previous === value ? null : value;\n message.feedback = next;\n\n try {\n if (next) {\n await transport.submitFeedback(agentId, threadId.value, message.basecampMessageId, next);\n } else {\n await transport.clearFeedback(agentId, threadId.value, message.basecampMessageId);\n }\n emitEvent({\n type: 'feedback',\n value: next,\n assistantMessageId: message.basecampMessageId,\n threadId: threadId.value,\n ...turnContext(message),\n });\n } catch {\n message.feedback = previous;\n }\n }\n\n /** Tells the app's event listener that a citation was opened. */\n function noteCitationClick(messageId: string, citation: ChatCitation): void {\n const message = findMessage(messageId);\n if (!message) return;\n emitEvent({\n type: 'citation-click',\n citation,\n assistantMessageId: message.basecampMessageId,\n threadId: threadId.value,\n ...turnContext(message),\n });\n }\n\n /** Clears a failed send so the user can try the same question again. */\n async function retry(): Promise<void> {\n const lastUser = [...messages.value].reverse().find((m) => m.role === 'user');\n if (!lastUser || busy.value) return;\n // Drop the failed exchange and resend the same question.\n messages.value = messages.value.filter(\n (m) => m !== lastUser && !(m.role === 'assistant' && m.failed),\n );\n error.value = null;\n await sendMessage(lastUser.text, lastUser.inputMode);\n }\n\n return {\n agent: readonly(agent),\n agentReady,\n threadId: readonly(threadId),\n messages,\n status: readonly(status),\n error: readonly(error),\n errorKind,\n busy,\n tokenUsage: readonly(tokenUsage),\n quota: readonly(quota),\n liveThinking,\n sendMessage,\n stopGeneration,\n newConversation,\n listThreads,\n loadThread,\n setFeedback,\n noteCitationClick,\n retry,\n };\n}\n\nexport type ChatEngine = ReturnType<typeof useChatEngine>;\n","/**\n * A stand-in for Base Camp, for tests and for developing the interface before\n * the real backend is reachable. It answers like the real thing does: a\n * streaming response delivered a few words at a time, with the same event\n * names on the wire, so the whole reading path gets exercised.\n */\nimport type { ChatTransport } from '../composables/useChatEngine';\nimport type {\n AgentDetails,\n ChatCitation,\n ChatMessage,\n CreateThreadBody,\n CreateThreadResponse,\n FeedbackResponse,\n MessageFeedback,\n MessageListResponse,\n QuotaResponse,\n SendMessageBody,\n StopMessageResponse,\n Thread,\n TokenUsage,\n UsedSource,\n} from '../types';\n\nexport interface MockReply {\n text: string;\n sources?: UsedSource[];\n /** Emitted as the citations event, the way inline-citation agents answer. */\n citations?: ChatCitation[];\n /** Streamed as thinking events ahead of the answer, like a reasoning agent. */\n thinking?: string;\n /** Emitted as the token_usage event after the answer completes. */\n tokenUsage?: TokenUsage;\n}\n\nexport interface MockTransportOptions {\n /** Decides what the assistant says back. Defaults to a canned answer. */\n replyTo?: (userText: string) => MockReply;\n /** Pause between streamed pieces, in milliseconds. */\n chunkDelayMs?: number;\n /** Pause before the first piece, imitating retrieval time. */\n thinkingDelayMs?: number;\n /**\n * A message allowance to report, counting down as messages are sent.\n * Left out, the mock answers 'unlimited', the way an uncapped tenant does.\n */\n messageLimit?: number;\n}\n\nconst DEFAULT_REPLY: MockReply = {\n text: [\n 'This is a **mock answer** from the development transport, standing in for the real backend.',\n '',\n 'It streams a few words at a time so the interface behaves the way it will in production:',\n '',\n '- markdown renders as it arrives',\n '- the thinking indicator shows before the first words',\n '- sources attach once the answer finishes',\n '',\n 'Send another message to see it again, or start a new chat to reset.',\n ].join('\\n'),\n sources: [],\n};\n\nfunction sse(event: string, data: unknown): string {\n return `event: ${event}\\ndata: ${JSON.stringify(data)}\\n\\n`;\n}\n\nconst sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));\n\nexport class MockTransport implements ChatTransport {\n private options: MockTransportOptions;\n\n private threadCount = 0;\n\n private messageCount = 0;\n\n // Exchanges recorded per thread, so the history methods answer like the\n // real backend and a history rail can be built against the mock.\n private history = new Map<string, ChatMessage[]>();\n\n /** Fixed for the life of the mock, so a test can assert on it. */\n private readonly quotaResetAt = new Date('2026-01-02T00:00:00.000Z').toISOString();\n\n constructor(options: MockTransportOptions = {}) {\n this.options = options;\n }\n\n async getAgent(agentId: string): Promise<AgentDetails> {\n return {\n id: agentId,\n name: 'Mock Agent',\n description: 'Development stand-in for a Base Camp agent.',\n config: {},\n } as AgentDetails;\n }\n\n async createThread(_agentId: string, _body?: CreateThreadBody): Promise<CreateThreadResponse> {\n this.threadCount += 1;\n const id = `mock-thread-${this.threadCount}`;\n this.history.set(id, []);\n return {\n id,\n agentId: _agentId,\n status: 'ACTIVE',\n createdAt: new Date().toISOString(),\n } as CreateThreadResponse;\n }\n\n /** Newest first and capped at `limit`, the way the server pages. */\n async getThreads(agentId: string, limit: number): Promise<Thread[]> {\n return [...this.history.entries()]\n .reverse()\n .slice(0, limit)\n .map(([id, messages]) => ({\n id,\n agentId,\n status: 'ACTIVE' as const,\n messageCount: messages.length,\n lastMessageAt: messages.at(-1)?.createdAt,\n }));\n }\n\n async getQuota(_agentId: string): Promise<QuotaResponse> {\n const limit = this.options.messageLimit;\n if (limit === undefined) {\n return { limit: 'unlimited', remaining: 'unlimited', resetAt: this.quotaResetAt };\n }\n // Two messages per exchange, but only the sender's own count against it.\n const sent = Math.ceil(this.messageCount / 2);\n return { limit, remaining: Math.max(0, limit - sent), resetAt: this.quotaResetAt };\n }\n\n async getMessages(_agentId: string, threadId: string): Promise<MessageListResponse> {\n const messages = this.history.get(threadId);\n if (!messages) {\n throw new Error(`Unknown thread: ${threadId}`);\n }\n return { threadId, messages };\n }\n\n async sendMessage(\n _agentId: string,\n _threadId: string,\n body: SendMessageBody,\n options?: { signal?: AbortSignal },\n ): Promise<Response> {\n const signal = options?.signal;\n const reply = this.options.replyTo?.(body.message) ?? DEFAULT_REPLY;\n const chunkDelay = this.options.chunkDelayMs ?? 30;\n const thinkingDelay = this.options.thinkingDelayMs ?? 600;\n\n this.messageCount += 1;\n const userId = `mock-msg-${this.messageCount}`;\n this.messageCount += 1;\n const replyId = `mock-msg-${this.messageCount}`;\n\n // Split the reply so each piece is a word or two, like real deltas.\n const pieces = reply.text.match(/\\S+\\s*/g) ?? [reply.text];\n const thinkingPieces = reply.thinking?.match(/\\S+\\s*/g) ?? [];\n\n // Recorded up front rather than as the stream is read: the mock knows the\n // whole exchange already, and history-after-cancel is not worth modelling.\n const now = new Date().toISOString();\n this.history.get(_threadId)?.push(\n { id: userId, role: 'user', text: body.message, createdAt: now },\n {\n id: replyId,\n role: 'assistant',\n text: reply.text,\n createdAt: now,\n sourceContext: reply.sources,\n metadata: reply.thinking ? { thinking: reply.thinking } : undefined,\n },\n );\n\n const encoder = new TextEncoder();\n const stream = new ReadableStream<Uint8Array>({\n async start(controller) {\n const send = (text: string) => controller.enqueue(encoder.encode(text));\n // Cancelling mid-answer behaves like a real aborted request.\n const throwIfAborted = () => {\n if (signal?.aborted) {\n throw new DOMException('The user cancelled the stream.', 'AbortError');\n }\n };\n try {\n // The field is messageId, matching the real server's payload shape.\n send(sse('message_id', { messageId: userId }));\n send(sse('assistant_message_id', { messageId: replyId }));\n // Reasoning streams during the retrieval wait, before any answer text.\n if (thinkingPieces.length > 0) {\n send(sse('thinking_start', {}));\n for (const piece of thinkingPieces) {\n throwIfAborted();\n send(sse('thinking_delta', { text: piece }));\n await sleep(chunkDelay);\n }\n send(sse('thinking_end', {}));\n }\n await sleep(thinkingDelay);\n for (const piece of pieces) {\n throwIfAborted();\n send(sse('response_delta', { text: piece }));\n await sleep(chunkDelay);\n }\n send(sse('text_complete', {}));\n // Citations follow text_complete, matching the real stream's order.\n if (reply.citations?.length) {\n send(sse('citations', reply.citations));\n }\n if (reply.sources?.length) {\n // The payload is the bare array, exactly as the real server sends\n // it. The mock once wrapped it in { sources }, which let a wrong\n // reading in the engine pass every test while dropping real ones.\n send(sse('used_sources', reply.sources));\n }\n if (reply.tokenUsage) {\n // The per-message half of the payload is not modelled; the engine\n // only reads the thread half.\n send(\n sse('token_usage', {\n message: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },\n thread: reply.tokenUsage,\n }),\n );\n }\n send('data: [DONE]\\n\\n');\n controller.close();\n } catch (err) {\n // Hand the cancellation to whoever is reading, then stop.\n try {\n controller.error(err);\n } catch {\n // Already closed.\n }\n }\n },\n });\n\n return new Response(stream, {\n status: 200,\n headers: { 'Content-Type': 'text/event-stream' },\n });\n }\n\n async stopMessage(): Promise<StopMessageResponse> {\n return { success: true, message: 'stopped' } as StopMessageResponse;\n }\n\n async submitFeedback(\n _agentId: string,\n _threadId: string,\n _messageId: string,\n feedback: MessageFeedback,\n ): Promise<FeedbackResponse> {\n return { feedback } as FeedbackResponse;\n }\n\n async clearFeedback(): Promise<FeedbackResponse> {\n return {} as FeedbackResponse;\n }\n}\n","/**\n * Voice dictation through the browser's built-in speech recognition.\n *\n * Chrome, Edge and Safari expose the Web Speech API (Safari under the webkit\n * prefix); Firefox has no implementation, so callers should hide their mic\n * button when `isSupported` is false. Chrome and Edge run recognition on\n * Google's servers rather than on the device, which matters to\n * privacy-sensitive products.\n *\n * The transcript is written into the caller's text ref as the user speaks,\n * after whatever was already typed. Nothing is submitted from here; the user\n * reviews the dictated text and sends it like a typed message. Dictation\n * stops itself after a stretch of silence, so the mic is not left hot while\n * the user reads their question back.\n */\nimport { onScopeDispose, ref, type Ref } from 'vue';\n\ninterface RecognitionResultEvent {\n resultIndex: number;\n results: ArrayLike<{ 0: { transcript: string }; isFinal: boolean }>;\n}\n\n/** The slice of the Web Speech API this composable touches. */\ninterface Recognition {\n continuous: boolean;\n interimResults: boolean;\n lang: string;\n onresult: ((event: RecognitionResultEvent) => void) | null;\n onerror: (() => void) | null;\n onend: (() => void) | null;\n start(): void;\n stop(): void;\n abort(): void;\n}\n\ntype RecognitionConstructor = new () => Recognition;\n\nfunction findConstructor(): RecognitionConstructor | null {\n if (typeof window === 'undefined') return null;\n const speech = window as {\n SpeechRecognition?: RecognitionConstructor;\n webkitSpeechRecognition?: RecognitionConstructor;\n };\n return speech.SpeechRecognition ?? speech.webkitSpeechRecognition ?? null;\n}\n\nexport interface DictationOptions {\n /** Recognition language, as a BCP 47 tag. */\n lang?: string;\n /** Milliseconds of silence before dictation stops itself. 0 turns this off. */\n idleMs?: number;\n}\n\n/** Long enough for a mid-sentence pause, short enough not to catch stray talk. */\nconst DEFAULT_IDLE_MS = 2000;\n\nexport default function useDictation(text: Ref<string>, options: DictationOptions = {}) {\n const Recognition = findConstructor();\n const idleMs = options.idleMs ?? DEFAULT_IDLE_MS;\n\n const isListening = ref(false);\n let recognition: Recognition | null = null;\n let idleTimer: ReturnType<typeof setTimeout> | undefined;\n /** What was typed before dictation started; the transcript lands after it. */\n let prefix = '';\n /** Speech the browser has finalized this session. Interim words still change. */\n let finals = '';\n\n // Speech shows up as a stream of result events; silence is their absence.\n function armIdleTimer(): void {\n if (!idleMs) return;\n clearTimeout(idleTimer);\n idleTimer = setTimeout(stop, idleMs);\n }\n\n function init(Ctor: RecognitionConstructor): Recognition {\n const instance = new Ctor();\n // Keep listening across pauses, and show words while they are provisional.\n instance.continuous = true;\n instance.interimResults = true;\n instance.lang = options.lang ?? 'en-US';\n\n instance.onresult = (event) => {\n // A result can trail in after stop; writing it would refill a cleared field.\n if (!isListening.value) return;\n let interim = '';\n armIdleTimer();\n for (let i = event.resultIndex; i < event.results.length; i += 1) {\n const result = event.results[i];\n if (!result) continue;\n if (result.isFinal) finals += `${result[0].transcript} `;\n else interim += result[0].transcript;\n }\n text.value = prefix + finals + interim;\n };\n // The browser can also end recognition on its own, or after an error.\n instance.onend = () => {\n clearTimeout(idleTimer);\n isListening.value = false;\n };\n instance.onerror = () => {\n clearTimeout(idleTimer);\n isListening.value = false;\n };\n return instance;\n }\n\n function start(): void {\n if (!Recognition || isListening.value) return;\n recognition ??= init(Recognition);\n prefix = text.value ? `${text.value.trim()} ` : '';\n finals = '';\n isListening.value = true;\n armIdleTimer();\n try {\n recognition.start();\n } catch {\n // Already running; the session in progress keeps going.\n }\n }\n\n function stop(): void {\n clearTimeout(idleTimer);\n isListening.value = false;\n recognition?.stop();\n }\n\n function toggle(): void {\n if (isListening.value) stop();\n else start();\n }\n\n // Unmounting the composer must release the microphone.\n onScopeDispose(() => {\n clearTimeout(idleTimer);\n recognition?.abort();\n });\n\n return {\n /** False where the browser has no speech recognition, notably Firefox. */\n isSupported: Recognition !== null,\n isListening,\n toggle,\n stop,\n };\n}\n","<script setup lang=\"ts\">\n/**\n * A round button holding a single icon, with the button's name shown in a\n * small chip above it while hovered.\n *\n * Used like a plain <button>: click handlers, `disabled`, and extra classes\n * all fall through to the button element.\n */\ndefineProps<{\n /** What the button does, e.g. \"Send\". Shown in the chip on hover. */\n label: string;\n}>();\n</script>\n\n<template>\n <button type=\"button\" class=\"icon-button\">\n <slot />\n <span class=\"icon-button__label\">{{ label }}</span>\n </button>\n</template>\n\n<style scoped>\n/* A caller gives a button its own colors (a recording mic, an open toggle) by\n setting --icon-button-state-bg / --icon-button-state-fg on it. Those win in\n every state, hovered included, without a specificity contest against the\n kit's rules here. */\n.icon-button {\n /* The label chip positions itself against this box. */\n position: relative;\n display: grid;\n place-items: center;\n width: 40px;\n height: 40px;\n padding: 0;\n border: none;\n border-radius: var(--r-pill);\n background: var(--icon-button-state-bg, var(--icon-button-bg));\n color: var(--icon-button-state-fg, var(--text-primary));\n cursor: pointer;\n}\n\n.icon-button:disabled {\n color: var(--text-secondary);\n cursor: default;\n}\n\n.icon-button:not(:disabled):hover {\n background: var(--icon-button-state-bg, var(--icon-button-bg-hover));\n}\n\n.icon-button__label {\n position: absolute;\n bottom: calc(100% + var(--spacing-1));\n left: 50%;\n transform: translateX(-50%);\n width: max-content;\n /* The design puts 15px around the label, between the spacing steps. */\n padding: 15px;\n border-radius: var(--card-radius);\n background: var(--surface-card);\n box-shadow: var(--card-shadow);\n color: var(--text-primary);\n font-family: var(--font-label);\n font-size: var(--t-xs);\n font-weight: var(--fw-bold);\n line-height: 1;\n text-transform: uppercase;\n white-space: nowrap;\n opacity: 0;\n visibility: hidden;\n transition: opacity 0.15s ease;\n /* Never steals the click aimed at the button under it. */\n pointer-events: none;\n}\n\n/* Hover is gated to devices that really have it. On touch screens a tap would\n otherwise leave the label stuck on until the next tap elsewhere. */\n@media (hover: hover) {\n .icon-button:not(:disabled):hover .icon-button__label {\n opacity: 1;\n visibility: visible;\n }\n}\n</style>\n","<script setup lang=\"ts\">\n/**\n * A round button holding a single icon, with the button's name shown in a\n * small chip above it while hovered.\n *\n * Used like a plain <button>: click handlers, `disabled`, and extra classes\n * all fall through to the button element.\n */\ndefineProps<{\n /** What the button does, e.g. \"Send\". Shown in the chip on hover. */\n label: string;\n}>();\n</script>\n\n<template>\n <button type=\"button\" class=\"icon-button\">\n <slot />\n <span class=\"icon-button__label\">{{ label }}</span>\n </button>\n</template>\n\n<style scoped>\n/* A caller gives a button its own colors (a recording mic, an open toggle) by\n setting --icon-button-state-bg / --icon-button-state-fg on it. Those win in\n every state, hovered included, without a specificity contest against the\n kit's rules here. */\n.icon-button {\n /* The label chip positions itself against this box. */\n position: relative;\n display: grid;\n place-items: center;\n width: 40px;\n height: 40px;\n padding: 0;\n border: none;\n border-radius: var(--r-pill);\n background: var(--icon-button-state-bg, var(--icon-button-bg));\n color: var(--icon-button-state-fg, var(--text-primary));\n cursor: pointer;\n}\n\n.icon-button:disabled {\n color: var(--text-secondary);\n cursor: default;\n}\n\n.icon-button:not(:disabled):hover {\n background: var(--icon-button-state-bg, var(--icon-button-bg-hover));\n}\n\n.icon-button__label {\n position: absolute;\n bottom: calc(100% + var(--spacing-1));\n left: 50%;\n transform: translateX(-50%);\n width: max-content;\n /* The design puts 15px around the label, between the spacing steps. */\n padding: 15px;\n border-radius: var(--card-radius);\n background: var(--surface-card);\n box-shadow: var(--card-shadow);\n color: var(--text-primary);\n font-family: var(--font-label);\n font-size: var(--t-xs);\n font-weight: var(--fw-bold);\n line-height: 1;\n text-transform: uppercase;\n white-space: nowrap;\n opacity: 0;\n visibility: hidden;\n transition: opacity 0.15s ease;\n /* Never steals the click aimed at the button under it. */\n pointer-events: none;\n}\n\n/* Hover is gated to devices that really have it. On touch screens a tap would\n otherwise leave the label stuck on until the next tap elsewhere. */\n@media (hover: hover) {\n .icon-button:not(:disabled):hover .icon-button__label {\n opacity: 1;\n visibility: visible;\n }\n}\n</style>\n","<script setup lang=\"ts\">\n/**\n * The kit's own small icon set, drawn inline so the package needs no icon\n * library from the app. Shapes for send, mic, and thumbs come from the\n * project's Figma file. Color always follows the surrounding text color.\n */\ndefineProps<{\n name:\n | 'send'\n | 'mic'\n | 'stop'\n | 'thumb-up'\n | 'thumb-down'\n | 'warning'\n | 'sparkle'\n | 'copy'\n | 'external-link'\n | 'chevron-down';\n size?: number;\n}>();\n</script>\n\n<template>\n<!-- Points right, not up: the design's send is a horizontal arrow. -->\n <svg\n v-if=\"name === 'send'\"\n :width=\"size ?? 16\"\n :height=\"size ?? 16\"\n viewBox=\"0 0 20 20\"\n fill=\"none\"\n >\n <path\n d=\"M4.16667 10L15.8333 10M10 15.8333L15.8333 10L10 4.16667\"\n stroke=\"currentColor\"\n stroke-width=\"1.5\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n />\n </svg>\n\n <svg\n v-else-if=\"name === 'mic'\"\n :width=\"size ?? 16\"\n :height=\"size ?? 16\"\n viewBox=\"0 0 10.88 16\"\n fill=\"currentColor\"\n >\n <path\n d=\"M8.65 3.168C8.65 1.418 7.213 0 5.44 0C3.667 0 2.23 1.418 2.23 3.168V7.049C2.23 8.799 3.667 10.217 5.44 10.217C7.213 10.217 8.65 8.799 8.65 7.049V3.168Z\"\n />\n <path\n d=\"M10.293 4.53C9.969 4.53 9.706 4.789 9.706 5.109V7.05C9.706 9.371 7.792 11.26 5.44 11.26C3.088 11.26 1.174 9.371 1.174 7.05V5.109C1.174 4.789 0.911 4.53 0.587 4.53C0.263 4.53 0 4.789 0 5.109V7.05C0 9.814 2.129 12.097 4.853 12.387V14.842H3.069C2.745 14.842 2.482 15.102 2.482 15.421C2.482 15.741 2.745 16 3.069 16H7.811C8.135 16 8.398 15.741 8.398 15.421C8.398 15.102 8.135 14.842 7.811 14.842H6.027V12.387C8.751 12.097 10.88 9.814 10.88 7.05V5.109C10.88 4.789 10.617 4.53 10.293 4.53Z\"\n />\n </svg>\n\n <svg\n v-else-if=\"name === 'stop'\"\n :width=\"size ?? 16\"\n :height=\"size ?? 16\"\n viewBox=\"0 0 16 16\"\n fill=\"currentColor\"\n >\n <rect x=\"3\" y=\"3\" width=\"10\" height=\"10\" rx=\"2\" />\n </svg>\n\n <svg\n v-else-if=\"name === 'thumb-up' || name === 'thumb-down'\"\n :width=\"size ?? 20\"\n :height=\"size ?? 20\"\n viewBox=\"0 0 20.02 16.3\"\n fill=\"currentColor\"\n :style=\"name === 'thumb-down' ? 'transform: rotate(180deg)' : undefined\"\n >\n <path\n d=\"M16.29 16.3C15.95 16.3 15.6 16.3 15.26 16.29H14.48C14.09 16.28 13.69 16.28 13.3 16.28H11.72C10.95 16.27 10.17 16.26 9.4 16.29C8.72 16.31 8.21 16 7.77 15.73C7.65 15.66 7.53 15.58 7.41 15.52C7.06 15.34 7.04 14.9 7.04 14.77C7.04 12.58 7.04 10.4 7.04 8.21C7.04 7.96 7.17 7.62 7.43 7.45C9.46 6.03 10.41 4 10.33 1.25C10.32 0.86 10.3 0.42 10.6 0.17C10.91 -0.09 11.34 0 11.74 0.09C12.64 0.28 13.25 0.87 13.54 1.82C13.92 3.02 13.79 4.25 13.61 5.39C13.58 5.55 13.55 5.71 13.54 5.82C13.68 5.82 13.88 5.81 13.99 5.81C15.08 5.72 16.18 5.79 17.26 5.85L17.71 5.88C18.16 5.91 18.57 6.07 18.97 6.26C19.9 6.69 20.23 7.49 19.88 8.46C19.81 8.65 19.72 8.87 19.58 9.07C19.52 9.15 19.52 9.16 19.58 9.27C20.18 10.24 20.03 10.93 19.07 11.64C19.08 11.63 19.07 11.7 19.07 11.76C19.07 11.86 19.08 11.96 19.1 12.06C19.13 12.21 19.15 12.37 19.15 12.52C19.15 13.12 18.89 13.59 18.39 13.92C18.39 13.92 18.33 13.98 18.26 14.2C18.21 14.33 18.18 14.47 18.16 14.61C18.11 14.83 18.06 15.05 17.97 15.27C17.69 15.93 17.1 16.3 16.3 16.3H16.29ZM2.71 16.22C2.22 16.22 1.73 16.22 1.24 16.22C0.41 16.22 0.01 15.82 0 15.01C0 14.13 0 13.25 0 12.36V8.29C0 7.46 0.43 7.04 1.25 7.04C2.4 7.04 3.56 7.04 4.71 7.04C5.55 7.04 5.97 7.44 5.97 8.28C5.97 10.5 5.97 12.73 5.97 14.95C5.97 15.82 5.58 16.22 4.69 16.22C4.02 16.22 3.36 16.22 2.7 16.22H2.71Z\"\n />\n </svg>\n\n <svg\n v-else-if=\"name === 'sparkle'\"\n :width=\"size ?? 16\"\n :height=\"((size ?? 16) * 14.7693) / 16\"\n viewBox=\"0 0 16.0001 14.7693\"\n fill=\"currentColor\"\n >\n <path\n d=\"M11.9472 8.83878C8.3169 9.81745 7.28765 10.867 6.32788 14.5687C6.25841 14.8362 5.88552 14.8362 5.81605 14.5687C4.85627 10.867 3.82703 9.81745 0.196726 8.83878C-0.0655754 8.76795 -0.0655754 8.38771 0.196726 8.31688C3.82703 7.33821 4.85627 6.2887 5.81605 2.58693C5.88552 2.31946 6.25841 2.31946 6.32788 2.58693C7.28765 6.2887 8.3169 7.33821 11.9472 8.31688C12.2095 8.38771 12.2095 8.76795 11.9472 8.83878ZM15.9001 3.0099C14.0576 2.51324 13.5354 1.98075 13.0483 0.101979C13.0132 -0.033993 12.8239 -0.033993 12.7884 0.101979C12.3013 1.98075 11.7791 2.51324 9.93662 3.0099C9.80327 3.04573 9.80327 3.23869 9.93662 3.27493C11.7791 3.77159 12.3013 4.30408 12.7884 6.18285C12.8235 6.31882 13.0128 6.31882 13.0483 6.18285C13.5354 4.30408 14.0576 3.77159 15.9001 3.27493C16.0334 3.2391 16.0334 3.04614 15.9001 3.0099Z\"\n />\n </svg>\n\n <!-- Two overlapping sheets; the design strokes the fill for a bolder line. -->\n <svg\n v-else-if=\"name === 'copy'\"\n :width=\"size ?? 16\"\n :height=\"size ?? 16\"\n viewBox=\"0 0 16 16\"\n fill=\"currentColor\"\n stroke=\"currentColor\"\n stroke-width=\"0.5\"\n >\n <path\n d=\"M3.62712 10.4832H2.41239C1.89338 10.4832 1.47374 10.0876 1.47374 9.59831V2.30569C1.47374 1.8164 1.89338 1.42079 2.41239 1.42079H9.24246C9.76148 1.42079 10.1811 1.8164 10.1811 2.30569V3.25826C10.1811 3.47168 10.3688 3.64866 10.5952 3.64866C10.8216 3.64866 11.0093 3.47168 11.0093 3.25826V2.30569C11.0093 1.38436 10.2142 0.64 9.24246 0.64H2.40687C1.43509 0.64 0.64 1.38956 0.64 2.30569V9.59311C0.64 10.5144 1.43509 11.2588 2.40687 11.2588H3.6216C3.84798 11.2588 4.03571 11.0818 4.03571 10.8684C4.03571 10.655 3.84798 10.478 3.6216 10.478L3.62712 10.4832Z\"\n />\n <path\n d=\"M13.5931 4.74136H6.76306C5.78576 4.74136 4.99619 5.49092 4.99619 6.40705V13.6945C4.99619 14.6158 5.79128 15.3602 6.76306 15.3602H13.5931C14.5704 15.3602 15.36 14.6106 15.36 13.6945V6.40705C15.36 5.48572 14.5649 4.74136 13.5931 4.74136ZM13.5931 14.5846H6.76306C6.24404 14.5846 5.82441 14.189 5.82441 13.6997V6.41226C5.82441 5.92296 6.24404 5.52736 6.76306 5.52736H13.5931C14.1121 5.52736 14.5318 5.92296 14.5318 6.41226V13.6997C14.5318 14.189 14.1121 14.5846 13.5931 14.5846Z\"\n />\n </svg>\n\n <!-- A box with an arrow leaving through its top right corner. -->\n <svg\n v-else-if=\"name === 'external-link'\"\n :width=\"size ?? 16\"\n :height=\"size ?? 16\"\n viewBox=\"0 0 16 16\"\n fill=\"currentColor\"\n stroke=\"currentColor\"\n stroke-width=\"0.5\"\n >\n <path\n d=\"M13.496 8.69167C13.218 8.69167 12.992 8.9176 12.992 9.19566V12.9668C12.992 13.6359 12.4446 14.1833 11.7755 14.1833H4.22447C3.55539 14.1833 3.00797 13.6359 3.00797 12.9668V5.42448C3.00797 4.75539 3.55539 4.20797 4.22447 4.20797H7.99566C8.27371 4.20797 8.49964 3.98204 8.49964 3.70398C8.49964 3.42592 8.27371 3.2 7.99566 3.2H4.22447C2.99928 3.2 2 4.19928 2 5.42448V12.9755C2 14.2007 2.99928 15.2 4.22447 15.2H11.7755C13.0007 15.2 14 14.2007 14 12.9755V9.20435C14 8.92629 13.7741 8.70036 13.496 8.70036V8.69167Z\"\n />\n <path\n d=\"M15.1084 1.6H10.0653C9.79408 1.6 9.57371 1.82066 9.57371 2.09225C9.57371 2.36383 9.79408 2.58449 10.0653 2.58449H13.9218L6.94621 9.56083C6.75126 9.75603 6.75126 10.0616 6.94621 10.2568C7.14115 10.452 7.45476 10.4435 7.64122 10.2568L14.6168 3.28043V7.14203C14.6168 7.41361 14.8372 7.63427 15.1084 7.63427C15.3796 7.63427 15.6 7.41361 15.6 7.14203V2.09225C15.6 1.82066 15.3796 1.6 15.1084 1.6Z\"\n />\n </svg>\n\n <svg\n v-else-if=\"name === 'chevron-down'\"\n :width=\"size ?? 16\"\n :height=\"size ?? 16\"\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n >\n <path\n d=\"M4 6l4 4 4-4\"\n stroke=\"currentColor\"\n stroke-width=\"1.5\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n />\n </svg>\n\n <svg\n v-else-if=\"name === 'warning'\"\n :width=\"size ?? 16\"\n :height=\"((size ?? 16) * 14.4) / 16\"\n viewBox=\"0 0 16 14.4\"\n fill=\"currentColor\"\n >\n <path\n d=\"M15.8568 12.7841L8.91585 0.538566C8.50877 -0.179522 7.49127 -0.179522 7.08419 0.538566L0.143286 12.7841C-0.263791 13.5022 0.244959 14.4 1.05911 14.4H14.9409C15.7551 14.4 16.2638 13.5022 15.8568 12.7841ZM7.62818 4.62113C7.871 4.47245 8.17448 4.47245 8.4173 4.62113C8.66763 4.77452 8.81032 5.05952 8.78548 5.35589L8.4329 9.54183C8.41037 9.79368 8.24188 9.95393 8.01716 9.95393C7.79244 9.95393 7.63511 9.79368 7.61258 9.54183L7.26 5.35589C7.23497 5.05952 7.37785 4.77452 7.62818 4.62113ZM8.87156 11.6941C8.87156 12.1406 8.5105 12.484 8.01716 12.484C7.52381 12.484 7.16276 12.1406 7.16276 11.6941V11.5796C7.16276 11.1332 7.53575 10.7897 8.01716 10.7897C8.49856 10.7897 8.87156 11.1332 8.87156 11.5796V11.6941Z\"\n />\n </svg>\n</template>\n","<script setup lang=\"ts\">\n/**\n * The kit's own small icon set, drawn inline so the package needs no icon\n * library from the app. Shapes for send, mic, and thumbs come from the\n * project's Figma file. Color always follows the surrounding text color.\n */\ndefineProps<{\n name:\n | 'send'\n | 'mic'\n | 'stop'\n | 'thumb-up'\n | 'thumb-down'\n | 'warning'\n | 'sparkle'\n | 'copy'\n | 'external-link'\n | 'chevron-down';\n size?: number;\n}>();\n</script>\n\n<template>\n<!-- Points right, not up: the design's send is a horizontal arrow. -->\n <svg\n v-if=\"name === 'send'\"\n :width=\"size ?? 16\"\n :height=\"size ?? 16\"\n viewBox=\"0 0 20 20\"\n fill=\"none\"\n >\n <path\n d=\"M4.16667 10L15.8333 10M10 15.8333L15.8333 10L10 4.16667\"\n stroke=\"currentColor\"\n stroke-width=\"1.5\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n />\n </svg>\n\n <svg\n v-else-if=\"name === 'mic'\"\n :width=\"size ?? 16\"\n :height=\"size ?? 16\"\n viewBox=\"0 0 10.88 16\"\n fill=\"currentColor\"\n >\n <path\n d=\"M8.65 3.168C8.65 1.418 7.213 0 5.44 0C3.667 0 2.23 1.418 2.23 3.168V7.049C2.23 8.799 3.667 10.217 5.44 10.217C7.213 10.217 8.65 8.799 8.65 7.049V3.168Z\"\n />\n <path\n d=\"M10.293 4.53C9.969 4.53 9.706 4.789 9.706 5.109V7.05C9.706 9.371 7.792 11.26 5.44 11.26C3.088 11.26 1.174 9.371 1.174 7.05V5.109C1.174 4.789 0.911 4.53 0.587 4.53C0.263 4.53 0 4.789 0 5.109V7.05C0 9.814 2.129 12.097 4.853 12.387V14.842H3.069C2.745 14.842 2.482 15.102 2.482 15.421C2.482 15.741 2.745 16 3.069 16H7.811C8.135 16 8.398 15.741 8.398 15.421C8.398 15.102 8.135 14.842 7.811 14.842H6.027V12.387C8.751 12.097 10.88 9.814 10.88 7.05V5.109C10.88 4.789 10.617 4.53 10.293 4.53Z\"\n />\n </svg>\n\n <svg\n v-else-if=\"name === 'stop'\"\n :width=\"size ?? 16\"\n :height=\"size ?? 16\"\n viewBox=\"0 0 16 16\"\n fill=\"currentColor\"\n >\n <rect x=\"3\" y=\"3\" width=\"10\" height=\"10\" rx=\"2\" />\n </svg>\n\n <svg\n v-else-if=\"name === 'thumb-up' || name === 'thumb-down'\"\n :width=\"size ?? 20\"\n :height=\"size ?? 20\"\n viewBox=\"0 0 20.02 16.3\"\n fill=\"currentColor\"\n :style=\"name === 'thumb-down' ? 'transform: rotate(180deg)' : undefined\"\n >\n <path\n d=\"M16.29 16.3C15.95 16.3 15.6 16.3 15.26 16.29H14.48C14.09 16.28 13.69 16.28 13.3 16.28H11.72C10.95 16.27 10.17 16.26 9.4 16.29C8.72 16.31 8.21 16 7.77 15.73C7.65 15.66 7.53 15.58 7.41 15.52C7.06 15.34 7.04 14.9 7.04 14.77C7.04 12.58 7.04 10.4 7.04 8.21C7.04 7.96 7.17 7.62 7.43 7.45C9.46 6.03 10.41 4 10.33 1.25C10.32 0.86 10.3 0.42 10.6 0.17C10.91 -0.09 11.34 0 11.74 0.09C12.64 0.28 13.25 0.87 13.54 1.82C13.92 3.02 13.79 4.25 13.61 5.39C13.58 5.55 13.55 5.71 13.54 5.82C13.68 5.82 13.88 5.81 13.99 5.81C15.08 5.72 16.18 5.79 17.26 5.85L17.71 5.88C18.16 5.91 18.57 6.07 18.97 6.26C19.9 6.69 20.23 7.49 19.88 8.46C19.81 8.65 19.72 8.87 19.58 9.07C19.52 9.15 19.52 9.16 19.58 9.27C20.18 10.24 20.03 10.93 19.07 11.64C19.08 11.63 19.07 11.7 19.07 11.76C19.07 11.86 19.08 11.96 19.1 12.06C19.13 12.21 19.15 12.37 19.15 12.52C19.15 13.12 18.89 13.59 18.39 13.92C18.39 13.92 18.33 13.98 18.26 14.2C18.21 14.33 18.18 14.47 18.16 14.61C18.11 14.83 18.06 15.05 17.97 15.27C17.69 15.93 17.1 16.3 16.3 16.3H16.29ZM2.71 16.22C2.22 16.22 1.73 16.22 1.24 16.22C0.41 16.22 0.01 15.82 0 15.01C0 14.13 0 13.25 0 12.36V8.29C0 7.46 0.43 7.04 1.25 7.04C2.4 7.04 3.56 7.04 4.71 7.04C5.55 7.04 5.97 7.44 5.97 8.28C5.97 10.5 5.97 12.73 5.97 14.95C5.97 15.82 5.58 16.22 4.69 16.22C4.02 16.22 3.36 16.22 2.7 16.22H2.71Z\"\n />\n </svg>\n\n <svg\n v-else-if=\"name === 'sparkle'\"\n :width=\"size ?? 16\"\n :height=\"((size ?? 16) * 14.7693) / 16\"\n viewBox=\"0 0 16.0001 14.7693\"\n fill=\"currentColor\"\n >\n <path\n d=\"M11.9472 8.83878C8.3169 9.81745 7.28765 10.867 6.32788 14.5687C6.25841 14.8362 5.88552 14.8362 5.81605 14.5687C4.85627 10.867 3.82703 9.81745 0.196726 8.83878C-0.0655754 8.76795 -0.0655754 8.38771 0.196726 8.31688C3.82703 7.33821 4.85627 6.2887 5.81605 2.58693C5.88552 2.31946 6.25841 2.31946 6.32788 2.58693C7.28765 6.2887 8.3169 7.33821 11.9472 8.31688C12.2095 8.38771 12.2095 8.76795 11.9472 8.83878ZM15.9001 3.0099C14.0576 2.51324 13.5354 1.98075 13.0483 0.101979C13.0132 -0.033993 12.8239 -0.033993 12.7884 0.101979C12.3013 1.98075 11.7791 2.51324 9.93662 3.0099C9.80327 3.04573 9.80327 3.23869 9.93662 3.27493C11.7791 3.77159 12.3013 4.30408 12.7884 6.18285C12.8235 6.31882 13.0128 6.31882 13.0483 6.18285C13.5354 4.30408 14.0576 3.77159 15.9001 3.27493C16.0334 3.2391 16.0334 3.04614 15.9001 3.0099Z\"\n />\n </svg>\n\n <!-- Two overlapping sheets; the design strokes the fill for a bolder line. -->\n <svg\n v-else-if=\"name === 'copy'\"\n :width=\"size ?? 16\"\n :height=\"size ?? 16\"\n viewBox=\"0 0 16 16\"\n fill=\"currentColor\"\n stroke=\"currentColor\"\n stroke-width=\"0.5\"\n >\n <path\n d=\"M3.62712 10.4832H2.41239C1.89338 10.4832 1.47374 10.0876 1.47374 9.59831V2.30569C1.47374 1.8164 1.89338 1.42079 2.41239 1.42079H9.24246C9.76148 1.42079 10.1811 1.8164 10.1811 2.30569V3.25826C10.1811 3.47168 10.3688 3.64866 10.5952 3.64866C10.8216 3.64866 11.0093 3.47168 11.0093 3.25826V2.30569C11.0093 1.38436 10.2142 0.64 9.24246 0.64H2.40687C1.43509 0.64 0.64 1.38956 0.64 2.30569V9.59311C0.64 10.5144 1.43509 11.2588 2.40687 11.2588H3.6216C3.84798 11.2588 4.03571 11.0818 4.03571 10.8684C4.03571 10.655 3.84798 10.478 3.6216 10.478L3.62712 10.4832Z\"\n />\n <path\n d=\"M13.5931 4.74136H6.76306C5.78576 4.74136 4.99619 5.49092 4.99619 6.40705V13.6945C4.99619 14.6158 5.79128 15.3602 6.76306 15.3602H13.5931C14.5704 15.3602 15.36 14.6106 15.36 13.6945V6.40705C15.36 5.48572 14.5649 4.74136 13.5931 4.74136ZM13.5931 14.5846H6.76306C6.24404 14.5846 5.82441 14.189 5.82441 13.6997V6.41226C5.82441 5.92296 6.24404 5.52736 6.76306 5.52736H13.5931C14.1121 5.52736 14.5318 5.92296 14.5318 6.41226V13.6997C14.5318 14.189 14.1121 14.5846 13.5931 14.5846Z\"\n />\n </svg>\n\n <!-- A box with an arrow leaving through its top right corner. -->\n <svg\n v-else-if=\"name === 'external-link'\"\n :width=\"size ?? 16\"\n :height=\"size ?? 16\"\n viewBox=\"0 0 16 16\"\n fill=\"currentColor\"\n stroke=\"currentColor\"\n stroke-width=\"0.5\"\n >\n <path\n d=\"M13.496 8.69167C13.218 8.69167 12.992 8.9176 12.992 9.19566V12.9668C12.992 13.6359 12.4446 14.1833 11.7755 14.1833H4.22447C3.55539 14.1833 3.00797 13.6359 3.00797 12.9668V5.42448C3.00797 4.75539 3.55539 4.20797 4.22447 4.20797H7.99566C8.27371 4.20797 8.49964 3.98204 8.49964 3.70398C8.49964 3.42592 8.27371 3.2 7.99566 3.2H4.22447C2.99928 3.2 2 4.19928 2 5.42448V12.9755C2 14.2007 2.99928 15.2 4.22447 15.2H11.7755C13.0007 15.2 14 14.2007 14 12.9755V9.20435C14 8.92629 13.7741 8.70036 13.496 8.70036V8.69167Z\"\n />\n <path\n d=\"M15.1084 1.6H10.0653C9.79408 1.6 9.57371 1.82066 9.57371 2.09225C9.57371 2.36383 9.79408 2.58449 10.0653 2.58449H13.9218L6.94621 9.56083C6.75126 9.75603 6.75126 10.0616 6.94621 10.2568C7.14115 10.452 7.45476 10.4435 7.64122 10.2568L14.6168 3.28043V7.14203C14.6168 7.41361 14.8372 7.63427 15.1084 7.63427C15.3796 7.63427 15.6 7.41361 15.6 7.14203V2.09225C15.6 1.82066 15.3796 1.6 15.1084 1.6Z\"\n />\n </svg>\n\n <svg\n v-else-if=\"name === 'chevron-down'\"\n :width=\"size ?? 16\"\n :height=\"size ?? 16\"\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n >\n <path\n d=\"M4 6l4 4 4-4\"\n stroke=\"currentColor\"\n stroke-width=\"1.5\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n />\n </svg>\n\n <svg\n v-else-if=\"name === 'warning'\"\n :width=\"size ?? 16\"\n :height=\"((size ?? 16) * 14.4) / 16\"\n viewBox=\"0 0 16 14.4\"\n fill=\"currentColor\"\n >\n <path\n d=\"M15.8568 12.7841L8.91585 0.538566C8.50877 -0.179522 7.49127 -0.179522 7.08419 0.538566L0.143286 12.7841C-0.263791 13.5022 0.244959 14.4 1.05911 14.4H14.9409C15.7551 14.4 16.2638 13.5022 15.8568 12.7841ZM7.62818 4.62113C7.871 4.47245 8.17448 4.47245 8.4173 4.62113C8.66763 4.77452 8.81032 5.05952 8.78548 5.35589L8.4329 9.54183C8.41037 9.79368 8.24188 9.95393 8.01716 9.95393C7.79244 9.95393 7.63511 9.79368 7.61258 9.54183L7.26 5.35589C7.23497 5.05952 7.37785 4.77452 7.62818 4.62113ZM8.87156 11.6941C8.87156 12.1406 8.5105 12.484 8.01716 12.484C7.52381 12.484 7.16276 12.1406 7.16276 11.6941V11.5796C7.16276 11.1332 7.53575 10.7897 8.01716 10.7897C8.49856 10.7897 8.87156 11.1332 8.87156 11.5796V11.6941Z\"\n />\n </svg>\n</template>\n","<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';\n\nimport { useChatCopy } from '../copy';\nimport useDictation from '../composables/useDictation';\nimport type { ChatInputMode } from '../types';\nimport IconButton from './IconButton.vue';\nimport KitIcon from './KitIcon.vue';\n\nconst props = defineProps<{\n /** True while an answer is streaming. Swaps the send button for a stop button. */\n busy?: boolean;\n /** Offers voice dictation, in browsers with speech recognition. */\n showMic?: boolean;\n}>();\n\nconst emit = defineEmits<{\n send: [text: string, mode: ChatInputMode];\n stop: [];\n}>();\n\nconst copy = useChatCopy();\n\nconst draft = ref('');\nconst field = ref<HTMLTextAreaElement | null>(null);\n\nconst {\n isSupported: canDictate,\n isListening,\n toggle: toggleDictation,\n stop: stopDictation,\n} = useDictation(draft);\n\nconst canSend = computed(() => draft.value.trim().length > 0 && !props.busy);\n\n// Voice means dictation contributed to this draft, not that the mic is still\n// on at send time; speakers often stop the mic before sending. The draft\n// changing while the mic listens is the closest signal to actual dictation.\nconst draftUsedVoice = ref(false);\nwatch(draft, (value) => {\n if (!value) {\n draftUsedVoice.value = false;\n } else if (isListening.value) {\n draftUsedVoice.value = true;\n }\n});\n\n// The field grows with the draft up to a limit, then scrolls inside itself.\nfunction resize() {\n const el = field.value;\n if (!el) return;\n el.style.height = 'auto';\n el.style.height = `${Math.min(el.scrollHeight, 100)}px`;\n}\n\n// Watching the draft covers typing and dictation with one path.\nwatch(draft, resize, { flush: 'post' });\n\n/** Re-measures when the field's width changes; see onMounted. */\nlet widthWatcher: ResizeObserver | null = null;\n\n// The first paint needs sizing too. A long placeholder wraps, and without this\n// the field would sit at one row and scroll until the first keystroke.\nonMounted(() => {\n resize();\n // Fonts load with swap, so the first measurement can use fallback metrics\n // and settle on the wrong number of lines.\n void document.fonts.ready.then(resize);\n\n const el = field.value;\n if (!el || typeof ResizeObserver === 'undefined') return;\n\n /*\n * Where the placeholder wraps depends on how wide the field is, and the\n * measurements above only happen once. Anything that changes the width\n * afterwards left the height showing the wrong number of lines: resizing\n * the window, a layout that settles after mount, or a font that arrives\n * after document.fonts.ready has already resolved.\n *\n * Only a change in width is worth reacting to. resize() sets the height\n * itself, so reacting to height as well would call it from its own effect.\n */\n let lastWidth = el.clientWidth;\n widthWatcher = new ResizeObserver(() => {\n if (el.clientWidth === lastWidth) return;\n lastWidth = el.clientWidth;\n resize();\n });\n widthWatcher.observe(el);\n});\n\nonBeforeUnmount(() => widthWatcher?.disconnect());\n\nfunction submit() {\n if (!canSend.value) return;\n stopDictation();\n const text = draft.value;\n const mode: ChatInputMode = draftUsedVoice.value ? 'voice' : 'typed';\n draft.value = '';\n draftUsedVoice.value = false;\n emit('send', text, mode);\n}\n\nfunction onKeydown(event: KeyboardEvent) {\n if (event.key === 'Enter' && !event.shiftKey) {\n event.preventDefault();\n submit();\n }\n}\n</script>\n\n<template>\n <div class=\"input-bar\">\n <div class=\"input-bar__field\">\n <textarea\n ref=\"field\"\n v-model=\"draft\"\n class=\"input-bar__textarea\"\n rows=\"1\"\n :placeholder=\"copy.composer.placeholder\"\n @keydown=\"onKeydown\"\n />\n\n <div class=\"input-bar__buttons\">\n <IconButton\n v-if=\"showMic && canDictate\"\n :label=\"copy.composer.dictateLabel\"\n :class=\"{ 'input-bar__button--listening': isListening }\"\n @click=\"toggleDictation\"\n >\n <KitIcon name=\"mic\" />\n </IconButton>\n <!-- The mic's spot in browsers without speech recognition, notably\n Firefox. Hovering explains why there is no working button here. -->\n <span\n v-else-if=\"showMic\"\n class=\"input-bar__mic-unsupported\"\n :data-tooltip=\"copy.composer.micUnsupported\"\n >\n <KitIcon name=\"warning\" />\n </span>\n\n <IconButton v-if=\"busy\" :label=\"copy.composer.stopLabel\" @click=\"emit('stop')\">\n <KitIcon name=\"stop\" />\n </IconButton>\n <IconButton\n v-else\n class=\"input-bar__send\"\n :label=\"copy.composer.sendLabel\"\n :disabled=\"!canSend\"\n @click=\"submit\"\n >\n <!-- The design draws this one at 20px, unlike the other composer icons. -->\n <KitIcon name=\"send\" :size=\"20\" />\n </IconButton>\n </div>\n </div>\n </div>\n</template>\n\n<style scoped>\n.input-bar {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-3);\n padding: var(--spacing-6);\n padding-top: 0;\n}\n\n.input-bar__field {\n display: flex;\n /* Centred, so a one-line question sits level with the buttons beside it. */\n align-items: center;\n gap: var(--spacing-5);\n padding: var(--spacing-6);\n border: 1px solid var(--card-stroke);\n border-radius: var(--card-radius);\n background: var(--composer-bg);\n}\n\n/* The accent border says the composer is active. focus-within rather than the\n textarea's own focus, so it holds while the buttons beside it are focused. */\n.input-bar__field:focus-within {\n border-color: var(--accent);\n}\n\n/* Ready to send: enabled means the draft has text, so the button takes the\n accent, and hovering dims it rather than turning it gray. */\n.input-bar__send:enabled {\n --icon-button-state-bg: var(--icon-button-bg-active);\n --icon-button-state-fg: var(--icon-button-fg-active);\n}\n\n.input-bar__send:enabled:hover {\n --icon-button-state-bg: var(--icon-button-bg-active-hover);\n}\n\n.input-bar__textarea {\n /* A line of text is exactly as tall as the field, so a single line centres\n itself. Text in a textarea sits at the top of its box, so any spare height\n would otherwise strand that line above the buttons beside it. Keep the two\n uses of this in step. */\n --composer-line: 32px;\n flex: 1;\n min-width: 0;\n max-height: 100px;\n min-height: var(--composer-line);\n /* No vertical padding: the line height already fills the field, so padding\n on top of it would make the box grow the moment the first character is\n typed. */\n padding: 0;\n border: none;\n resize: none;\n background: none;\n color: var(--composer-fg);\n font-family: var(--font-body);\n font-size: var(--t-base);\n font-weight: var(--fw-regular);\n line-height: var(--composer-line);\n}\n\n.input-bar__textarea::placeholder {\n color: var(--composer-placeholder);\n}\n\n.input-bar__textarea:focus {\n outline: none;\n}\n\n.input-bar__buttons {\n display: flex;\n align-items: center;\n gap: var(--spacing-2);\n flex-shrink: 0;\n}\n\n.input-bar__mic-unsupported {\n position: relative;\n display: grid;\n place-items: center;\n width: 40px;\n height: 40px;\n color: var(--text-secondary);\n cursor: help;\n}\n\n/* The explanation, shown above the icon while hovered. */\n.input-bar__mic-unsupported::after {\n content: attr(data-tooltip);\n position: absolute;\n bottom: calc(100% + var(--spacing-2));\n /* Anchored to the icon's right edge so it grows leftward, into the field. */\n right: 0;\n width: max-content;\n max-width: 240px;\n padding: var(--spacing-2) var(--spacing-3);\n border: 1px solid var(--card-stroke);\n border-radius: var(--r-md);\n background: var(--surface-card);\n box-shadow: var(--card-shadow);\n color: var(--text-primary);\n font-size: var(--t-sm);\n line-height: var(--lh-sm);\n opacity: 0;\n visibility: hidden;\n transition: opacity 0.15s ease;\n pointer-events: none;\n}\n\n.input-bar__mic-unsupported:hover::after {\n opacity: 1;\n visibility: visible;\n}\n\n/* Recording. The active accent plus a slow pulse says the mic is live. The\n state properties keep the accent through hover; see IconButton. */\n.input-bar__button--listening {\n --icon-button-state-bg: var(--icon-button-bg-active);\n --icon-button-state-fg: var(--icon-button-fg-active);\n animation: mic-pulse 1.6s ease-in-out infinite;\n}\n\n@keyframes mic-pulse {\n 50% {\n opacity: 0.65;\n }\n}\n</style>\n","<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';\n\nimport { useChatCopy } from '../copy';\nimport useDictation from '../composables/useDictation';\nimport type { ChatInputMode } from '../types';\nimport IconButton from './IconButton.vue';\nimport KitIcon from './KitIcon.vue';\n\nconst props = defineProps<{\n /** True while an answer is streaming. Swaps the send button for a stop button. */\n busy?: boolean;\n /** Offers voice dictation, in browsers with speech recognition. */\n showMic?: boolean;\n}>();\n\nconst emit = defineEmits<{\n send: [text: string, mode: ChatInputMode];\n stop: [];\n}>();\n\nconst copy = useChatCopy();\n\nconst draft = ref('');\nconst field = ref<HTMLTextAreaElement | null>(null);\n\nconst {\n isSupported: canDictate,\n isListening,\n toggle: toggleDictation,\n stop: stopDictation,\n} = useDictation(draft);\n\nconst canSend = computed(() => draft.value.trim().length > 0 && !props.busy);\n\n// Voice means dictation contributed to this draft, not that the mic is still\n// on at send time; speakers often stop the mic before sending. The draft\n// changing while the mic listens is the closest signal to actual dictation.\nconst draftUsedVoice = ref(false);\nwatch(draft, (value) => {\n if (!value) {\n draftUsedVoice.value = false;\n } else if (isListening.value) {\n draftUsedVoice.value = true;\n }\n});\n\n// The field grows with the draft up to a limit, then scrolls inside itself.\nfunction resize() {\n const el = field.value;\n if (!el) return;\n el.style.height = 'auto';\n el.style.height = `${Math.min(el.scrollHeight, 100)}px`;\n}\n\n// Watching the draft covers typing and dictation with one path.\nwatch(draft, resize, { flush: 'post' });\n\n/** Re-measures when the field's width changes; see onMounted. */\nlet widthWatcher: ResizeObserver | null = null;\n\n// The first paint needs sizing too. A long placeholder wraps, and without this\n// the field would sit at one row and scroll until the first keystroke.\nonMounted(() => {\n resize();\n // Fonts load with swap, so the first measurement can use fallback metrics\n // and settle on the wrong number of lines.\n void document.fonts.ready.then(resize);\n\n const el = field.value;\n if (!el || typeof ResizeObserver === 'undefined') return;\n\n /*\n * Where the placeholder wraps depends on how wide the field is, and the\n * measurements above only happen once. Anything that changes the width\n * afterwards left the height showing the wrong number of lines: resizing\n * the window, a layout that settles after mount, or a font that arrives\n * after document.fonts.ready has already resolved.\n *\n * Only a change in width is worth reacting to. resize() sets the height\n * itself, so reacting to height as well would call it from its own effect.\n */\n let lastWidth = el.clientWidth;\n widthWatcher = new ResizeObserver(() => {\n if (el.clientWidth === lastWidth) return;\n lastWidth = el.clientWidth;\n resize();\n });\n widthWatcher.observe(el);\n});\n\nonBeforeUnmount(() => widthWatcher?.disconnect());\n\nfunction submit() {\n if (!canSend.value) return;\n stopDictation();\n const text = draft.value;\n const mode: ChatInputMode = draftUsedVoice.value ? 'voice' : 'typed';\n draft.value = '';\n draftUsedVoice.value = false;\n emit('send', text, mode);\n}\n\nfunction onKeydown(event: KeyboardEvent) {\n if (event.key === 'Enter' && !event.shiftKey) {\n event.preventDefault();\n submit();\n }\n}\n</script>\n\n<template>\n <div class=\"input-bar\">\n <div class=\"input-bar__field\">\n <textarea\n ref=\"field\"\n v-model=\"draft\"\n class=\"input-bar__textarea\"\n rows=\"1\"\n :placeholder=\"copy.composer.placeholder\"\n @keydown=\"onKeydown\"\n />\n\n <div class=\"input-bar__buttons\">\n <IconButton\n v-if=\"showMic && canDictate\"\n :label=\"copy.composer.dictateLabel\"\n :class=\"{ 'input-bar__button--listening': isListening }\"\n @click=\"toggleDictation\"\n >\n <KitIcon name=\"mic\" />\n </IconButton>\n <!-- The mic's spot in browsers without speech recognition, notably\n Firefox. Hovering explains why there is no working button here. -->\n <span\n v-else-if=\"showMic\"\n class=\"input-bar__mic-unsupported\"\n :data-tooltip=\"copy.composer.micUnsupported\"\n >\n <KitIcon name=\"warning\" />\n </span>\n\n <IconButton v-if=\"busy\" :label=\"copy.composer.stopLabel\" @click=\"emit('stop')\">\n <KitIcon name=\"stop\" />\n </IconButton>\n <IconButton\n v-else\n class=\"input-bar__send\"\n :label=\"copy.composer.sendLabel\"\n :disabled=\"!canSend\"\n @click=\"submit\"\n >\n <!-- The design draws this one at 20px, unlike the other composer icons. -->\n <KitIcon name=\"send\" :size=\"20\" />\n </IconButton>\n </div>\n </div>\n </div>\n</template>\n\n<style scoped>\n.input-bar {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-3);\n padding: var(--spacing-6);\n padding-top: 0;\n}\n\n.input-bar__field {\n display: flex;\n /* Centred, so a one-line question sits level with the buttons beside it. */\n align-items: center;\n gap: var(--spacing-5);\n padding: var(--spacing-6);\n border: 1px solid var(--card-stroke);\n border-radius: var(--card-radius);\n background: var(--composer-bg);\n}\n\n/* The accent border says the composer is active. focus-within rather than the\n textarea's own focus, so it holds while the buttons beside it are focused. */\n.input-bar__field:focus-within {\n border-color: var(--accent);\n}\n\n/* Ready to send: enabled means the draft has text, so the button takes the\n accent, and hovering dims it rather than turning it gray. */\n.input-bar__send:enabled {\n --icon-button-state-bg: var(--icon-button-bg-active);\n --icon-button-state-fg: var(--icon-button-fg-active);\n}\n\n.input-bar__send:enabled:hover {\n --icon-button-state-bg: var(--icon-button-bg-active-hover);\n}\n\n.input-bar__textarea {\n /* A line of text is exactly as tall as the field, so a single line centres\n itself. Text in a textarea sits at the top of its box, so any spare height\n would otherwise strand that line above the buttons beside it. Keep the two\n uses of this in step. */\n --composer-line: 32px;\n flex: 1;\n min-width: 0;\n max-height: 100px;\n min-height: var(--composer-line);\n /* No vertical padding: the line height already fills the field, so padding\n on top of it would make the box grow the moment the first character is\n typed. */\n padding: 0;\n border: none;\n resize: none;\n background: none;\n color: var(--composer-fg);\n font-family: var(--font-body);\n font-size: var(--t-base);\n font-weight: var(--fw-regular);\n line-height: var(--composer-line);\n}\n\n.input-bar__textarea::placeholder {\n color: var(--composer-placeholder);\n}\n\n.input-bar__textarea:focus {\n outline: none;\n}\n\n.input-bar__buttons {\n display: flex;\n align-items: center;\n gap: var(--spacing-2);\n flex-shrink: 0;\n}\n\n.input-bar__mic-unsupported {\n position: relative;\n display: grid;\n place-items: center;\n width: 40px;\n height: 40px;\n color: var(--text-secondary);\n cursor: help;\n}\n\n/* The explanation, shown above the icon while hovered. */\n.input-bar__mic-unsupported::after {\n content: attr(data-tooltip);\n position: absolute;\n bottom: calc(100% + var(--spacing-2));\n /* Anchored to the icon's right edge so it grows leftward, into the field. */\n right: 0;\n width: max-content;\n max-width: 240px;\n padding: var(--spacing-2) var(--spacing-3);\n border: 1px solid var(--card-stroke);\n border-radius: var(--r-md);\n background: var(--surface-card);\n box-shadow: var(--card-shadow);\n color: var(--text-primary);\n font-size: var(--t-sm);\n line-height: var(--lh-sm);\n opacity: 0;\n visibility: hidden;\n transition: opacity 0.15s ease;\n pointer-events: none;\n}\n\n.input-bar__mic-unsupported:hover::after {\n opacity: 1;\n visibility: visible;\n}\n\n/* Recording. The active accent plus a slow pulse says the mic is live. The\n state properties keep the accent through hover; see IconButton. */\n.input-bar__button--listening {\n --icon-button-state-bg: var(--icon-button-bg-active);\n --icon-button-state-fg: var(--icon-button-fg-active);\n animation: mic-pulse 1.6s ease-in-out infinite;\n}\n\n@keyframes mic-pulse {\n 50% {\n opacity: 0.65;\n }\n}\n</style>\n","/**\n * Keeps a scrolling element following new content, without ever fighting the\n * user.\n *\n * Two states. While 'following', new content pulls the element down with a\n * small glide each frame, so a new line slides up instead of snapping. Any\n * upward scroll switches to 'manual', which stops all movement until the user\n * returns to the bottom on their own; that switches back to 'following'.\n *\n * Following stops early when an `anchor` is given: the glide will not carry\n * that element above the top of the view. An answer longer than the window\n * therefore settles with its own beginning in view and grows out of sight\n * below, so the reader can start reading rather than chasing the last line.\n * Reading on past that point hands the view over to them, and following only\n * takes it back when they reach the bottom.\n */\nimport { nextTick, onBeforeUnmount, ref, watch, type Ref } from 'vue';\n\nimport type { AutoScrollMode } from '../types';\n\ntype ScrollState = 'following' | 'manual';\n\n// How close to the bottom counts as being there.\nconst BOTTOM_THRESHOLD_PX = 20;\n// Distances beyond this snap instantly, so opening a thread or returning to a\n// background tab does not animate through the whole backlog.\nconst SNAP_OVER_PX = 300;\n// Fraction of the remaining distance covered per frame while gliding.\nconst GLIDE_EASE = 0.3;\n// Breathing room left above the anchor when following stops against it.\nconst ANCHOR_GAP_PX = 12;\n/**\n * How far past its own stopping point the view must be before that counts as\n * the reader asking for the rest.\n *\n * Intent is read from position rather than from which events look\n * programmatic: the glide never scrolls beyond its own target, so anything\n * past it came from the reader. A browser gives no guarantee that a scroll\n * event caused by setting scrollTop arrives before the frame that marks the\n * write as ours, so a flag alone would sometimes mistake the glide for the\n * reader and lift the limit it had just applied.\n */\nconst PAST_TARGET_PX = 8;\n\nexport interface AutoScrollOptions {\n /**\n * How far to follow. Read through a getter rather than taken once, so an\n * app can change its mind, which is also why the listeners below stay\n * attached even while following is off. Defaults to 'reply'.\n */\n mode?: () => AutoScrollMode;\n /**\n * Resolves the element that following must not carry off the top, checked\n * fresh each frame because the answer it belongs to is still being built.\n * Returning null follows the bottom, as it did before this existed.\n */\n anchor?: () => HTMLElement | null;\n}\n\nexport default function useAutoScroll(\n elementRef: Ref<HTMLElement | null>,\n options: AutoScrollOptions = {},\n) {\n const state = ref<ScrollState>('following');\n let isProgrammaticScroll = false;\n let lastScrollTop = 0;\n let hasScrolledDownSinceManual = false;\n let glideRafId: number | null = null;\n /**\n * Set when the user scrolls down themselves, which asks for the rest of the\n * answer and so gives up the anchor limit until the next question.\n */\n let wantsBottom = false;\n\n const mode = (): AutoScrollMode => options.mode?.() ?? 'reply';\n\n function isAtBottom(el: HTMLElement): boolean {\n return el.scrollTop + el.clientHeight >= el.scrollHeight - BOTTOM_THRESHOLD_PX - 1;\n }\n\n // Scroll direction decides the state. The programmatic flag stops the\n // glide's own writes from looking like the user scrolling.\n function onScroll(): void {\n const el = elementRef.value;\n if (!el) return;\n\n const currentTop = el.scrollTop;\n const scrolledUp = currentTop < lastScrollTop;\n const scrolledDown = currentTop > lastScrollTop;\n lastScrollTop = currentTop;\n\n if (isProgrammaticScroll) return;\n\n if (scrolledUp) {\n state.value = 'manual';\n hasScrolledDownSinceManual = false;\n } else if (state.value === 'manual' && scrolledDown) {\n hasScrolledDownSinceManual = true;\n } else if (\n scrolledDown &&\n state.value === 'following' &&\n currentTop > followTarget(el) + PAST_TARGET_PX\n ) {\n // Reading on past where following stopped. Stand aside completely\n // rather than taking the view over: they set the pace from here, and\n // following waits until they reach the bottom themselves.\n state.value = 'manual';\n hasScrolledDownSinceManual = true;\n }\n\n if (state.value === 'manual' && hasScrolledDownSinceManual && isAtBottom(el)) {\n state.value = 'following';\n // Back at the bottom by their own hand; snapping up to the anchor now\n // would undo the scroll they just made.\n wantsBottom = true;\n hasScrolledDownSinceManual = false;\n }\n }\n\n // Wheel and touch don't move scrollTop when already at the edge, so the\n // intent has to be read from the gesture itself.\n function onWheel(event: WheelEvent): void {\n if (event.deltaY < 0) {\n state.value = 'manual';\n hasScrolledDownSinceManual = false;\n } else if (event.deltaY > 0 && state.value === 'manual') {\n hasScrolledDownSinceManual = true;\n }\n // Downward gestures need no case of their own: when following has stopped\n // against the anchor there is room below, so the gesture moves scrollTop\n // and onScroll reads the intent from the new position.\n }\n\n let touchStartY = 0;\n\n function onTouchStart(event: TouchEvent): void {\n const touch = event.touches[0];\n if (touch) touchStartY = touch.clientY;\n }\n\n function onTouchMove(event: TouchEvent): void {\n const touch = event.touches[0];\n if (!touch) return;\n const currentY = touch.clientY;\n if (currentY > touchStartY) {\n // Finger moved down, so the content scrolls up.\n state.value = 'manual';\n hasScrolledDownSinceManual = false;\n } else if (currentY < touchStartY && state.value === 'manual') {\n hasScrolledDownSinceManual = true;\n }\n }\n\n function attachListeners(el: HTMLElement): void {\n el.addEventListener('scroll', onScroll, { passive: true });\n el.addEventListener('wheel', onWheel, { passive: true });\n el.addEventListener('touchstart', onTouchStart, { passive: true });\n el.addEventListener('touchmove', onTouchMove, { passive: true });\n lastScrollTop = el.scrollTop;\n }\n\n function detachListeners(el: HTMLElement): void {\n el.removeEventListener('scroll', onScroll);\n el.removeEventListener('wheel', onWheel);\n el.removeEventListener('touchstart', onTouchStart);\n el.removeEventListener('touchmove', onTouchMove);\n }\n\n let currentEl: HTMLElement | null = null;\n\n // Watching the ref keeps this working when the element renders conditionally.\n watch(\n elementRef,\n (newEl, oldEl) => {\n if (oldEl) detachListeners(oldEl);\n if (newEl) attachListeners(newEl);\n currentEl = newEl ?? null;\n },\n { immediate: true },\n );\n\n onBeforeUnmount(() => {\n if (currentEl) detachListeners(currentEl);\n currentEl = null;\n if (glideRafId !== null) {\n cancelAnimationFrame(glideRafId);\n glideRafId = null;\n }\n });\n\n /**\n * How far down following is allowed to go: the bottom, unless an anchor\n * would be carried off the top before then.\n *\n * Measured from the anchor's position in the scrolled content, so it moves\n * with the answer as it grows. A short answer never reaches the limit and\n * follows the bottom as before.\n */\n function followTarget(el: HTMLElement): number {\n const bottom = el.scrollHeight - el.clientHeight;\n // 'bottom' keeps the newest text in view, so no anchor holds it back.\n if (wantsBottom || mode() === 'bottom') return bottom;\n\n const anchor = options.anchor?.();\n if (!anchor) return bottom;\n\n const offsetInContent =\n anchor.getBoundingClientRect().top - el.getBoundingClientRect().top + el.scrollTop;\n return Math.min(bottom, offsetInContent - ANCHOR_GAP_PX);\n }\n\n /**\n * Smoothly follow new content while it streams in. Each frame closes a\n * fraction of the remaining distance. Distances beyond SNAP_OVER_PX jump\n * instantly, and the glide stops the moment the user scrolls away.\n */\n function glideToBottom(): void {\n if (mode() === 'off') return;\n void nextTick(() => {\n // A glide already in flight picks up the new target on its next frame.\n if (glideRafId !== null) return;\n\n const step = () => {\n glideRafId = null;\n const el = elementRef.value;\n if (!el || state.value !== 'following') return;\n\n const target = followTarget(el);\n // Never negative: following only ever moves the view down, so an\n // anchor already above the top is left where the reader put it.\n const delta = target - el.scrollTop;\n if (delta <= 0) return;\n\n isProgrammaticScroll = true;\n if (delta > SNAP_OVER_PX || delta <= 1) {\n el.scrollTop = target;\n } else {\n el.scrollTop += Math.max(1, delta * GLIDE_EASE);\n glideRafId = requestAnimationFrame(step);\n }\n requestAnimationFrame(() => {\n isProgrammaticScroll = false;\n });\n };\n glideRafId = requestAnimationFrame(step);\n });\n }\n\n /**\n * Forces following again, for when the user clearly wants the bottom. Also\n * re-arms the anchor limit, so each new question gets to settle with its\n * answer's beginning in view.\n */\n function resetToFollowing(): void {\n state.value = 'following';\n wantsBottom = false;\n }\n\n return { glideToBottom, resetToFollowing };\n}\n","/**\n * Turns `[^N]` citation markers into numbered chips.\n *\n * Agents with inline citations write footnote markers into the answer text,\n * and the matching passages arrive separately on the citations stream event.\n * This swaps each resolved marker in the rendered HTML for a small clickable\n * chip carrying its citation's ref, which the message component looks up when\n * a chip is clicked.\n *\n * The swap runs on sanitized HTML, after markdown: `[^N]` means nothing to\n * the markdown parser or the sanitizer, so it survives both intact. The chip\n * markup built here is fully escaped, so it needs no second sanitize pass.\n */\nimport { decodeHtmlEntities } from './entities';\nimport type { ChatCitation } from '../types';\n\nconst CITATION_MARKER_PATTERN = /\\[\\^(\\d+)\\]/g;\n\nfunction escapeAttr(value: string): string {\n return value.replace(/&/g, '&').replace(/\"/g, '"');\n}\n\nexport interface InjectCitationChipsOptions {\n /**\n * Chips markers whose citations have not arrived yet, as inert placeholders.\n * The citations payload only arrives once the text finishes, so without this\n * a streaming answer shows raw `[^N]` text that snaps into chips at the very\n * end. Placeholders upgrade to real chips when their citation lands; on by\n * the caller only while streaming, so a truly unresolved marker still ends\n * up as literal text.\n */\n pendingMarkers?: boolean;\n}\n\n/**\n * Stands in for the number until the citation arrives. The marker's own ref is\n * deliberately not shown: refs run in retrieval order (63, 61, 23...) while the\n * chip displays a dense label (1, 2, 3...), so printing the ref would show the\n * wrong number and visibly correct itself a moment later.\n */\nconst PENDING_CHIP = '<sup class=\"citation-chip citation-chip--pending\">·</sup>';\n\n/**\n * Replaces resolved `[^N]` markers in `html` with citation chips. A marker\n * with no matching citation is left as literal text, so a hallucinated ref\n * never becomes a dead chip.\n */\nexport function injectCitationChips(\n html: string,\n citations: ChatCitation[],\n options: InjectCitationChipsOptions = {},\n): string {\n if (!html) return html;\n if (citations.length === 0 && !options.pendingMarkers) return html;\n\n const byRef = new Map(citations.map((citation) => [citation.ref, citation]));\n return html.replace(CITATION_MARKER_PATTERN, (marker, refText: string) => {\n const citation = byRef.get(Number(refText));\n if (citation) {\n // Decode first, then escape for the attribute: the tooltip should read\n // \"Ratings & Specs\", not the encoded form the retrieval text carries.\n const title = escapeAttr(decodeHtmlEntities(citation.title || 'Source'));\n return (\n `<sup class=\"citation-chip\" data-citation-ref=\"${citation.ref}\" ` +\n `title=\"${title}\" tabindex=\"0\">${citation.label}</sup>`\n );\n }\n if (options.pendingMarkers) return PENDING_CHIP;\n return marker;\n });\n}\n","<script setup lang=\"ts\">\n/**\n * The card that opens when a citation chip is clicked: which document the\n * passage came from, the passage itself, and buttons to copy or open the\n * source. Positioning is owned by the parent; this only draws the card.\n */\nimport { computed, onBeforeUnmount, ref, watch } from 'vue';\n\nimport { useChatCopy } from '../copy';\nimport { decodeHtmlEntities } from '../services/entities';\nimport type { ChatCitation } from '../types';\nimport KitIcon from './KitIcon.vue';\n\nconst props = defineProps<{\n citation: ChatCitation;\n}>();\n\nconst emit = defineEmits<{ close: [] }>();\n\nconst copy = useChatCopy();\n\n/**\n * The page image is served from a signed URL that can fail (an expired token,\n * or a source with no page scan behind it), and a broken image is worse than\n * no image, so a failure drops the thumbnail entirely.\n */\nconst imageFailed = ref(false);\nwatch(\n () => props.citation.screenshotUrl,\n () => {\n imageFailed.value = false;\n },\n);\n\nconst showImage = computed(() => Boolean(props.citation.screenshotUrl) && !imageFailed.value);\n\n// Retrieved text arrives with markup-special characters encoded; both of these\n// are rendered as text, so the references have to be decoded first.\nconst passage = computed(() => decodeHtmlEntities(props.citation.passage));\nconst title = computed(() => decodeHtmlEntities(props.citation.title || 'Source'));\n\nfunction pageLine(): string | null {\n const page = props.citation.textPage ?? props.citation.page;\n if (page === undefined) return null;\n return `p. ${page}`;\n}\n\n/**\n * What the copy button hands over. Deliberately not `link`, which Base Camp\n * turns into a signed redirect that records the click: it is scoped to one\n * message and expires, so pasting it into an email shares a tracking URL that\n * will later break rather than the document. `canonicalLink` is the document\n * itself. Replies from before tracking existed carry only `link`, and there it\n * is the plain URL, so it remains the right fallback.\n */\nconst shareUrl = computed(() => props.citation.canonicalLink ?? props.citation.link);\n\n// The button says \"copied\" for a moment, since nothing else visibly happens.\nconst copied = ref(false);\nlet copiedTimer: ReturnType<typeof setTimeout> | undefined;\n\nasync function copyUrl(): Promise<void> {\n if (!shareUrl.value) return;\n try {\n await navigator.clipboard.writeText(shareUrl.value);\n copied.value = true;\n clearTimeout(copiedTimer);\n copiedTimer = setTimeout(() => {\n copied.value = false;\n }, 2000);\n } catch {\n // The browser can refuse clipboard access; the label just stays put.\n }\n}\n\nonBeforeUnmount(() => clearTimeout(copiedTimer));\n</script>\n\n<template>\n <div class=\"citation-popover\" @keydown.esc=\"emit('close')\">\n <div class=\"citation-popover__source\">\n <img\n v-if=\"showImage\"\n class=\"citation-popover__thumb\"\n :src=\"citation.screenshotUrl\"\n @error=\"imageFailed = true\"\n />\n <span class=\"citation-popover__badge\">{{ citation.label }}</span>\n <div class=\"citation-popover__source-text\">\n <p class=\"citation-popover__title\">{{ title }}</p>\n <p v-if=\"pageLine()\" class=\"citation-popover__page\">{{ pageLine() }}</p>\n </div>\n </div>\n\n <div class=\"citation-popover__passage-block\">\n <p class=\"citation-popover__passage-label\">{{ copy.sources.citedPassageLabel }}</p>\n <blockquote class=\"citation-popover__passage\">{{ passage }}</blockquote>\n </div>\n\n <p v-if=\"copy.sources.noticeLine\" class=\"citation-popover__notice\">\n {{ copy.sources.noticeLine }}\n </p>\n\n <div v-if=\"citation.link\" class=\"citation-popover__footer\">\n <button type=\"button\" class=\"citation-popover__copy\" @click=\"copyUrl\">\n {{ copied ? copy.sources.copiedLabel : copy.sources.copyUrlLabel }}\n <KitIcon name=\"copy\" />\n </button>\n <a\n class=\"citation-popover__open\"\n :href=\"citation.link\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n >\n {{ copy.sources.openSourceLabel }}\n <KitIcon name=\"external-link\" />\n </a>\n </div>\n </div>\n</template>\n\n<style scoped>\n/* The caller caps max-height to the space beside the chip, so the card never\n runs off screen. Everything but the passage keeps its size; the passage\n takes the slack and scrolls, leaving the source and the link always in view. */\n.citation-popover {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-5);\n width: min(350px, 100%);\n padding: var(--spacing-6);\n border: 1px solid var(--card-stroke);\n border-radius: var(--card-radius);\n background: var(--surface-card);\n box-shadow: var(--card-shadow);\n /* Scrolls itself if a cramped viewport squeezes even the header and link.\n `contain` stops a scroll that this card cannot use from escaping to the\n page behind it, which would shift the chat and dismiss the card. */\n overflow-y: auto;\n overscroll-behavior: contain;\n scrollbar-width: thin;\n scrollbar-color: var(--scrollbar-thumb) transparent;\n}\n\n.citation-popover__source {\n display: flex;\n align-items: flex-start;\n gap: var(--spacing-2);\n flex-shrink: 0;\n}\n\n/* A scan of the cited page. Absent for a source with no page behind it. */\n.citation-popover__thumb {\n display: block;\n width: 64px;\n height: 72px;\n margin-right: var(--spacing-2);\n flex-shrink: 0;\n border: 1px solid var(--card-stroke);\n border-radius: var(--r-lg);\n object-fit: cover;\n /* A document page is mostly whitespace, so anchor to the top: the heading\n is what makes the page recognizable, not its middle. */\n object-position: top center;\n}\n\n.citation-popover__badge {\n display: grid;\n place-items: center;\n min-width: 20px;\n height: 20px;\n padding: 0 5px;\n border-radius: var(--r-md);\n background: var(--citation-bg);\n color: var(--accent-fg);\n font-family: var(--font-body);\n font-size: var(--t-xs);\n font-weight: var(--fw-bold);\n flex-shrink: 0;\n}\n\n.citation-popover__source-text {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-1);\n min-width: 0;\n}\n\n.citation-popover__title {\n color: var(--text-primary);\n font-family: var(--font-body);\n font-size: var(--t-sm);\n font-weight: var(--fw-bold);\n overflow-wrap: break-word;\n}\n\n.citation-popover__page {\n color: var(--text-helper);\n font-family: var(--font-body);\n font-size: var(--t-sm);\n}\n\n.citation-popover__passage-block {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-2);\n flex: 1;\n min-height: 0;\n}\n\n.citation-popover__passage-label {\n color: var(--text-helper);\n font-family: var(--font-label);\n font-size: var(--t-xs);\n font-weight: var(--fw-bold);\n text-transform: uppercase;\n}\n\n.citation-popover__passage {\n margin: 0;\n padding-left: var(--spacing-6);\n border-left: 2px solid var(--accent);\n color: var(--text-primary);\n font-family: var(--font-body);\n font-size: var(--t-sm);\n line-height: var(--lh-body);\n overflow-y: auto;\n overscroll-behavior: contain;\n min-height: 0;\n scrollbar-width: thin;\n scrollbar-color: var(--scrollbar-thumb) transparent;\n}\n\n.citation-popover__notice {\n color: var(--text-helper);\n font-family: var(--font-body);\n font-size: var(--t-xs);\n line-height: var(--lh-body);\n /* The notice copy is client-locked down to its spacing, so consecutive\n spaces must survive rendering instead of collapsing. */\n white-space: pre-wrap;\n}\n\n.citation-popover__footer {\n display: flex;\n gap: var(--spacing-4);\n flex-shrink: 0;\n}\n\n/* The two footer buttons share one shape and split the row between them. */\n.citation-popover__copy,\n.citation-popover__open {\n display: flex;\n align-items: center;\n justify-content: center;\n gap: var(--spacing-2);\n flex: 1;\n height: 35px;\n padding: 0 var(--spacing-4);\n border-radius: var(--r-md);\n font-family: var(--font-ui);\n font-size: var(--t-sm);\n font-weight: var(--fw-bold);\n cursor: pointer;\n}\n\n.citation-popover__copy {\n border: 1px solid var(--card-stroke);\n background: none;\n color: var(--text-primary);\n}\n\n.citation-popover__open {\n border: none;\n background: var(--accent);\n color: var(--accent-fg);\n text-decoration: none;\n}\n</style>\n","<script setup lang=\"ts\">\n/**\n * The card that opens when a citation chip is clicked: which document the\n * passage came from, the passage itself, and buttons to copy or open the\n * source. Positioning is owned by the parent; this only draws the card.\n */\nimport { computed, onBeforeUnmount, ref, watch } from 'vue';\n\nimport { useChatCopy } from '../copy';\nimport { decodeHtmlEntities } from '../services/entities';\nimport type { ChatCitation } from '../types';\nimport KitIcon from './KitIcon.vue';\n\nconst props = defineProps<{\n citation: ChatCitation;\n}>();\n\nconst emit = defineEmits<{ close: [] }>();\n\nconst copy = useChatCopy();\n\n/**\n * The page image is served from a signed URL that can fail (an expired token,\n * or a source with no page scan behind it), and a broken image is worse than\n * no image, so a failure drops the thumbnail entirely.\n */\nconst imageFailed = ref(false);\nwatch(\n () => props.citation.screenshotUrl,\n () => {\n imageFailed.value = false;\n },\n);\n\nconst showImage = computed(() => Boolean(props.citation.screenshotUrl) && !imageFailed.value);\n\n// Retrieved text arrives with markup-special characters encoded; both of these\n// are rendered as text, so the references have to be decoded first.\nconst passage = computed(() => decodeHtmlEntities(props.citation.passage));\nconst title = computed(() => decodeHtmlEntities(props.citation.title || 'Source'));\n\nfunction pageLine(): string | null {\n const page = props.citation.textPage ?? props.citation.page;\n if (page === undefined) return null;\n return `p. ${page}`;\n}\n\n/**\n * What the copy button hands over. Deliberately not `link`, which Base Camp\n * turns into a signed redirect that records the click: it is scoped to one\n * message and expires, so pasting it into an email shares a tracking URL that\n * will later break rather than the document. `canonicalLink` is the document\n * itself. Replies from before tracking existed carry only `link`, and there it\n * is the plain URL, so it remains the right fallback.\n */\nconst shareUrl = computed(() => props.citation.canonicalLink ?? props.citation.link);\n\n// The button says \"copied\" for a moment, since nothing else visibly happens.\nconst copied = ref(false);\nlet copiedTimer: ReturnType<typeof setTimeout> | undefined;\n\nasync function copyUrl(): Promise<void> {\n if (!shareUrl.value) return;\n try {\n await navigator.clipboard.writeText(shareUrl.value);\n copied.value = true;\n clearTimeout(copiedTimer);\n copiedTimer = setTimeout(() => {\n copied.value = false;\n }, 2000);\n } catch {\n // The browser can refuse clipboard access; the label just stays put.\n }\n}\n\nonBeforeUnmount(() => clearTimeout(copiedTimer));\n</script>\n\n<template>\n <div class=\"citation-popover\" @keydown.esc=\"emit('close')\">\n <div class=\"citation-popover__source\">\n <img\n v-if=\"showImage\"\n class=\"citation-popover__thumb\"\n :src=\"citation.screenshotUrl\"\n @error=\"imageFailed = true\"\n />\n <span class=\"citation-popover__badge\">{{ citation.label }}</span>\n <div class=\"citation-popover__source-text\">\n <p class=\"citation-popover__title\">{{ title }}</p>\n <p v-if=\"pageLine()\" class=\"citation-popover__page\">{{ pageLine() }}</p>\n </div>\n </div>\n\n <div class=\"citation-popover__passage-block\">\n <p class=\"citation-popover__passage-label\">{{ copy.sources.citedPassageLabel }}</p>\n <blockquote class=\"citation-popover__passage\">{{ passage }}</blockquote>\n </div>\n\n <p v-if=\"copy.sources.noticeLine\" class=\"citation-popover__notice\">\n {{ copy.sources.noticeLine }}\n </p>\n\n <div v-if=\"citation.link\" class=\"citation-popover__footer\">\n <button type=\"button\" class=\"citation-popover__copy\" @click=\"copyUrl\">\n {{ copied ? copy.sources.copiedLabel : copy.sources.copyUrlLabel }}\n <KitIcon name=\"copy\" />\n </button>\n <a\n class=\"citation-popover__open\"\n :href=\"citation.link\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n >\n {{ copy.sources.openSourceLabel }}\n <KitIcon name=\"external-link\" />\n </a>\n </div>\n </div>\n</template>\n\n<style scoped>\n/* The caller caps max-height to the space beside the chip, so the card never\n runs off screen. Everything but the passage keeps its size; the passage\n takes the slack and scrolls, leaving the source and the link always in view. */\n.citation-popover {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-5);\n width: min(350px, 100%);\n padding: var(--spacing-6);\n border: 1px solid var(--card-stroke);\n border-radius: var(--card-radius);\n background: var(--surface-card);\n box-shadow: var(--card-shadow);\n /* Scrolls itself if a cramped viewport squeezes even the header and link.\n `contain` stops a scroll that this card cannot use from escaping to the\n page behind it, which would shift the chat and dismiss the card. */\n overflow-y: auto;\n overscroll-behavior: contain;\n scrollbar-width: thin;\n scrollbar-color: var(--scrollbar-thumb) transparent;\n}\n\n.citation-popover__source {\n display: flex;\n align-items: flex-start;\n gap: var(--spacing-2);\n flex-shrink: 0;\n}\n\n/* A scan of the cited page. Absent for a source with no page behind it. */\n.citation-popover__thumb {\n display: block;\n width: 64px;\n height: 72px;\n margin-right: var(--spacing-2);\n flex-shrink: 0;\n border: 1px solid var(--card-stroke);\n border-radius: var(--r-lg);\n object-fit: cover;\n /* A document page is mostly whitespace, so anchor to the top: the heading\n is what makes the page recognizable, not its middle. */\n object-position: top center;\n}\n\n.citation-popover__badge {\n display: grid;\n place-items: center;\n min-width: 20px;\n height: 20px;\n padding: 0 5px;\n border-radius: var(--r-md);\n background: var(--citation-bg);\n color: var(--accent-fg);\n font-family: var(--font-body);\n font-size: var(--t-xs);\n font-weight: var(--fw-bold);\n flex-shrink: 0;\n}\n\n.citation-popover__source-text {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-1);\n min-width: 0;\n}\n\n.citation-popover__title {\n color: var(--text-primary);\n font-family: var(--font-body);\n font-size: var(--t-sm);\n font-weight: var(--fw-bold);\n overflow-wrap: break-word;\n}\n\n.citation-popover__page {\n color: var(--text-helper);\n font-family: var(--font-body);\n font-size: var(--t-sm);\n}\n\n.citation-popover__passage-block {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-2);\n flex: 1;\n min-height: 0;\n}\n\n.citation-popover__passage-label {\n color: var(--text-helper);\n font-family: var(--font-label);\n font-size: var(--t-xs);\n font-weight: var(--fw-bold);\n text-transform: uppercase;\n}\n\n.citation-popover__passage {\n margin: 0;\n padding-left: var(--spacing-6);\n border-left: 2px solid var(--accent);\n color: var(--text-primary);\n font-family: var(--font-body);\n font-size: var(--t-sm);\n line-height: var(--lh-body);\n overflow-y: auto;\n overscroll-behavior: contain;\n min-height: 0;\n scrollbar-width: thin;\n scrollbar-color: var(--scrollbar-thumb) transparent;\n}\n\n.citation-popover__notice {\n color: var(--text-helper);\n font-family: var(--font-body);\n font-size: var(--t-xs);\n line-height: var(--lh-body);\n /* The notice copy is client-locked down to its spacing, so consecutive\n spaces must survive rendering instead of collapsing. */\n white-space: pre-wrap;\n}\n\n.citation-popover__footer {\n display: flex;\n gap: var(--spacing-4);\n flex-shrink: 0;\n}\n\n/* The two footer buttons share one shape and split the row between them. */\n.citation-popover__copy,\n.citation-popover__open {\n display: flex;\n align-items: center;\n justify-content: center;\n gap: var(--spacing-2);\n flex: 1;\n height: 35px;\n padding: 0 var(--spacing-4);\n border-radius: var(--r-md);\n font-family: var(--font-ui);\n font-size: var(--t-sm);\n font-weight: var(--fw-bold);\n cursor: pointer;\n}\n\n.citation-popover__copy {\n border: 1px solid var(--card-stroke);\n background: none;\n color: var(--text-primary);\n}\n\n.citation-popover__open {\n border: none;\n background: var(--accent);\n color: var(--accent-fg);\n text-decoration: none;\n}\n</style>\n","<script setup lang=\"ts\">\nimport { useChatCopy } from '../copy';\nimport KitIcon from './KitIcon.vue';\nimport type { MessageFeedback } from '../types';\n\ndefineProps<{ feedback: MessageFeedback | null }>();\n\nconst emit = defineEmits<{ rate: [value: MessageFeedback] }>();\n\nconst copy = useChatCopy();\n</script>\n\n<template>\n <div class=\"feedback\">\n <span class=\"feedback__question\">{{ copy.feedback.question }}</span>\n <div class=\"feedback__thumbs\">\n <button\n type=\"button\"\n class=\"feedback__thumb\"\n :class=\"{ 'is-active': feedback === 'POSITIVE' }\"\n @click=\"emit('rate', 'POSITIVE')\"\n >\n <KitIcon name=\"thumb-up\" />\n </button>\n <button\n type=\"button\"\n class=\"feedback__thumb\"\n :class=\"{ 'is-active': feedback === 'NEGATIVE' }\"\n @click=\"emit('rate', 'NEGATIVE')\"\n >\n <KitIcon name=\"thumb-down\" />\n </button>\n </div>\n </div>\n</template>\n\n<style scoped>\n.feedback {\n display: flex;\n align-items: center;\n justify-content: flex-end;\n gap: var(--spacing-4);\n width: 100%;\n}\n\n.feedback__question {\n color: var(--text-helper);\n font-family: var(--font-label);\n font-size: var(--t-xs);\n font-weight: var(--fw-bold);\n text-transform: uppercase;\n}\n\n.feedback__thumbs {\n display: flex;\n gap: var(--spacing-4);\n}\n\n.feedback__thumb {\n display: grid;\n place-items: center;\n padding: 2px;\n border: none;\n background: none;\n color: var(--text-helper);\n cursor: pointer;\n}\n\n/* Hover only says the control can be clicked. The chosen rating is the accent,\n and the two must not look alike: sharing one colour made a cleared rating\n look unchanged, because the pointer is still on the thumb that cleared it. */\n.feedback__thumb:hover {\n color: var(--text-primary);\n}\n\n/* Last, so a rated thumb keeps the accent while hovered. Same specificity. */\n.feedback__thumb.is-active {\n color: var(--accent);\n}\n</style>\n","<script setup lang=\"ts\">\nimport { useChatCopy } from '../copy';\nimport KitIcon from './KitIcon.vue';\nimport type { MessageFeedback } from '../types';\n\ndefineProps<{ feedback: MessageFeedback | null }>();\n\nconst emit = defineEmits<{ rate: [value: MessageFeedback] }>();\n\nconst copy = useChatCopy();\n</script>\n\n<template>\n <div class=\"feedback\">\n <span class=\"feedback__question\">{{ copy.feedback.question }}</span>\n <div class=\"feedback__thumbs\">\n <button\n type=\"button\"\n class=\"feedback__thumb\"\n :class=\"{ 'is-active': feedback === 'POSITIVE' }\"\n @click=\"emit('rate', 'POSITIVE')\"\n >\n <KitIcon name=\"thumb-up\" />\n </button>\n <button\n type=\"button\"\n class=\"feedback__thumb\"\n :class=\"{ 'is-active': feedback === 'NEGATIVE' }\"\n @click=\"emit('rate', 'NEGATIVE')\"\n >\n <KitIcon name=\"thumb-down\" />\n </button>\n </div>\n </div>\n</template>\n\n<style scoped>\n.feedback {\n display: flex;\n align-items: center;\n justify-content: flex-end;\n gap: var(--spacing-4);\n width: 100%;\n}\n\n.feedback__question {\n color: var(--text-helper);\n font-family: var(--font-label);\n font-size: var(--t-xs);\n font-weight: var(--fw-bold);\n text-transform: uppercase;\n}\n\n.feedback__thumbs {\n display: flex;\n gap: var(--spacing-4);\n}\n\n.feedback__thumb {\n display: grid;\n place-items: center;\n padding: 2px;\n border: none;\n background: none;\n color: var(--text-helper);\n cursor: pointer;\n}\n\n/* Hover only says the control can be clicked. The chosen rating is the accent,\n and the two must not look alike: sharing one colour made a cleared rating\n look unchanged, because the pointer is still on the thumb that cleared it. */\n.feedback__thumb:hover {\n color: var(--text-primary);\n}\n\n/* Last, so a rated thumb keeps the accent while hovered. Same specificity. */\n.feedback__thumb.is-active {\n color: var(--accent);\n}\n</style>\n","<script setup lang=\"ts\">\n/**\n * An enlarged view of a cited page, opened from a source card's thumbnail.\n * Deliberately dumb: the caller hands in finished strings and owns whether\n * the dialog exists at all, so this only draws and asks to be closed.\n */\nimport { onBeforeUnmount, onMounted } from 'vue';\n\nimport { useChatCopy } from '../copy';\n\ndefineProps<{\n url: string;\n /** The document's name, shown above the image. */\n title: string;\n /** The page line, as the card already displays it. Empty hides the line. */\n detail?: string;\n}>();\n\nconst emit = defineEmits<{ close: [] }>();\n\nconst copy = useChatCopy();\n\nfunction onKeydown(event: KeyboardEvent): void {\n if (event.key === 'Escape') emit('close');\n}\n\n// On the document, because nothing inside the dialog holds focus.\nonMounted(() => document.addEventListener('keydown', onKeydown));\nonBeforeUnmount(() => document.removeEventListener('keydown', onKeydown));\n</script>\n\n<template>\n <Teleport to=\"body\">\n <!-- Clicking the backdrop closes; clicking the card does not. -->\n <div class=\"page-image-modal\" @click.self=\"emit('close')\">\n <div class=\"page-image-modal__card\">\n <div class=\"page-image-modal__header\">\n <div class=\"page-image-modal__heading\">\n <p class=\"page-image-modal__title\">{{ title }}</p>\n <p v-if=\"detail\" class=\"page-image-modal__detail\">{{ detail }}</p>\n </div>\n <button type=\"button\" class=\"page-image-modal__close\" @click=\"emit('close')\">\n {{ copy.sources.closeLabel }}\n </button>\n </div>\n <img class=\"page-image-modal__image\" :src=\"url\" />\n </div>\n </div>\n </Teleport>\n</template>\n\n<style scoped>\n.page-image-modal {\n position: fixed;\n inset: 0;\n z-index: 200;\n display: grid;\n place-items: center;\n padding: var(--spacing-6);\n background: var(--overlay-scrim);\n}\n\n.page-image-modal__card {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-4);\n max-width: min(720px, 100%);\n max-height: 100%;\n padding: var(--spacing-6);\n border: 1px solid var(--card-stroke);\n border-radius: var(--card-radius);\n background: var(--surface-card);\n box-shadow: var(--card-shadow);\n}\n\n.page-image-modal__header {\n display: flex;\n align-items: flex-start;\n justify-content: space-between;\n gap: var(--spacing-4);\n}\n\n.page-image-modal__title {\n color: var(--text-primary);\n font-family: var(--font-body);\n font-size: var(--t-sm);\n font-weight: var(--fw-bold);\n overflow-wrap: break-word;\n}\n\n.page-image-modal__detail {\n color: var(--text-helper);\n font-family: var(--font-body);\n font-size: var(--t-sm);\n}\n\n.page-image-modal__close {\n padding: var(--spacing-1) var(--spacing-3);\n border: 1px solid var(--card-stroke);\n border-radius: var(--r-md);\n background: none;\n color: var(--text-primary);\n font-family: var(--font-ui);\n font-size: var(--t-sm);\n font-weight: var(--fw-bold);\n cursor: pointer;\n}\n\n.page-image-modal__close:hover {\n background: var(--surface-hover);\n}\n\n/* The page keeps its own proportions and scrolls when taller than the card. */\n.page-image-modal__image {\n min-height: 0;\n overflow-y: auto;\n border: 1px solid var(--card-stroke);\n border-radius: var(--r-lg);\n object-fit: contain;\n}\n</style>\n","<script setup lang=\"ts\">\n/**\n * An enlarged view of a cited page, opened from a source card's thumbnail.\n * Deliberately dumb: the caller hands in finished strings and owns whether\n * the dialog exists at all, so this only draws and asks to be closed.\n */\nimport { onBeforeUnmount, onMounted } from 'vue';\n\nimport { useChatCopy } from '../copy';\n\ndefineProps<{\n url: string;\n /** The document's name, shown above the image. */\n title: string;\n /** The page line, as the card already displays it. Empty hides the line. */\n detail?: string;\n}>();\n\nconst emit = defineEmits<{ close: [] }>();\n\nconst copy = useChatCopy();\n\nfunction onKeydown(event: KeyboardEvent): void {\n if (event.key === 'Escape') emit('close');\n}\n\n// On the document, because nothing inside the dialog holds focus.\nonMounted(() => document.addEventListener('keydown', onKeydown));\nonBeforeUnmount(() => document.removeEventListener('keydown', onKeydown));\n</script>\n\n<template>\n <Teleport to=\"body\">\n <!-- Clicking the backdrop closes; clicking the card does not. -->\n <div class=\"page-image-modal\" @click.self=\"emit('close')\">\n <div class=\"page-image-modal__card\">\n <div class=\"page-image-modal__header\">\n <div class=\"page-image-modal__heading\">\n <p class=\"page-image-modal__title\">{{ title }}</p>\n <p v-if=\"detail\" class=\"page-image-modal__detail\">{{ detail }}</p>\n </div>\n <button type=\"button\" class=\"page-image-modal__close\" @click=\"emit('close')\">\n {{ copy.sources.closeLabel }}\n </button>\n </div>\n <img class=\"page-image-modal__image\" :src=\"url\" />\n </div>\n </div>\n </Teleport>\n</template>\n\n<style scoped>\n.page-image-modal {\n position: fixed;\n inset: 0;\n z-index: 200;\n display: grid;\n place-items: center;\n padding: var(--spacing-6);\n background: var(--overlay-scrim);\n}\n\n.page-image-modal__card {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-4);\n max-width: min(720px, 100%);\n max-height: 100%;\n padding: var(--spacing-6);\n border: 1px solid var(--card-stroke);\n border-radius: var(--card-radius);\n background: var(--surface-card);\n box-shadow: var(--card-shadow);\n}\n\n.page-image-modal__header {\n display: flex;\n align-items: flex-start;\n justify-content: space-between;\n gap: var(--spacing-4);\n}\n\n.page-image-modal__title {\n color: var(--text-primary);\n font-family: var(--font-body);\n font-size: var(--t-sm);\n font-weight: var(--fw-bold);\n overflow-wrap: break-word;\n}\n\n.page-image-modal__detail {\n color: var(--text-helper);\n font-family: var(--font-body);\n font-size: var(--t-sm);\n}\n\n.page-image-modal__close {\n padding: var(--spacing-1) var(--spacing-3);\n border: 1px solid var(--card-stroke);\n border-radius: var(--r-md);\n background: none;\n color: var(--text-primary);\n font-family: var(--font-ui);\n font-size: var(--t-sm);\n font-weight: var(--fw-bold);\n cursor: pointer;\n}\n\n.page-image-modal__close:hover {\n background: var(--surface-hover);\n}\n\n/* The page keeps its own proportions and scrolls when taller than the card. */\n.page-image-modal__image {\n min-height: 0;\n overflow-y: auto;\n border: 1px solid var(--card-stroke);\n border-radius: var(--r-lg);\n object-fit: contain;\n}\n</style>\n","<script setup lang=\"ts\">\n/**\n * One document an answer drew on: a folded card with the document's name,\n * cover, and link on the front, and the cited passages inside. The fold keeps\n * a many-source answer readable; a reader opens the card they care about.\n */\nimport { computed, ref, watch } from 'vue';\n\nimport { formatCopy, useChatCopy } from '../copy';\nimport { decodeHtmlEntities, stripHtmlTags } from '../services/entities';\nimport type { SourceResult, UsedSource } from '../types';\nimport KitIcon from './KitIcon.vue';\n\nconst props = defineProps<{\n source: UsedSource;\n /** The card's position in the list, for its \"Source N\" heading. */\n index: number;\n}>();\n\nconst emit = defineEmits<{\n /** A passage thumbnail was clicked; the list decides how to show the page. */\n viewPage: [payload: { url: string; title: string; detail: string }];\n}>();\n\nconst copy = useChatCopy();\n\nconst expanded = ref(false);\n\nconst title = computed(() => decodeHtmlEntities(props.source.metadata?.title ?? ''));\nconst link = computed(() => props.source.metadata?.link || null);\n\n// Older threads persisted the cover at the source's root; see UsedSource.\nconst coverUrl = computed(\n () => props.source.metadata?.screenshotUrl || props.source.screenshotUrl || '',\n);\n\n// Cover URLs are short-lived signed links; a broken image is worse than none.\nconst coverFailed = ref(false);\nwatch(coverUrl, () => {\n coverFailed.value = false;\n});\n\n/** Retrieval hits with something to show; an empty hit would render a blank row. */\nconst references = computed(() =>\n props.source.results.filter((result) => result.content?.trim() || result.metadata?.screenshotUrl),\n);\n\nfunction passageOf(result: SourceResult): string {\n // Stripped before decoding, so an author's own \"<p>\" survives as text.\n return decodeHtmlEntities(stripHtmlTags(result.content ?? '')).trim();\n}\n\n/** \"Page 12\" or \"Page 12, Text page 10\", from the copy templates. */\nfunction pageLineOf(result: SourceResult): string {\n const { page, textPage } = result.metadata ?? {};\n return [\n page !== undefined ? formatCopy(copy.sources.pageLabel, { page }) : '',\n textPage !== undefined ? formatCopy(copy.sources.textPageLabel, { textPage }) : '',\n ]\n .filter(Boolean)\n .join(', ');\n}\n\nfunction viewPage(result: SourceResult): void {\n const url = result.metadata?.screenshotUrl;\n if (!url) return;\n emit('viewPage', {\n url,\n title: title.value || formatCopy(copy.sources.sourceLabel, { n: props.index + 1 }),\n detail: pageLineOf(result),\n });\n}\n</script>\n\n<template>\n <div class=\"source-card\">\n <!-- Sits over the top right corner rather than in the flow, so the\n document's name starts at the card's edge and the control stays put\n wherever the name wraps to. -->\n <button\n type=\"button\"\n class=\"source-card__toggle\"\n :class=\"{ 'source-card__toggle--open': expanded }\"\n :aria-expanded=\"expanded\"\n :aria-label=\"expanded ? copy.sources.collapseLabel : copy.sources.expandLabel\"\n @click=\"expanded = !expanded\"\n >\n <KitIcon name=\"chevron-down\" />\n </button>\n\n <div class=\"source-card__front\">\n <p class=\"source-card__heading\">\n {{ formatCopy(copy.sources.sourceLabel, { n: index + 1 }) }}\n </p>\n\n <div class=\"source-card__details\">\n <img\n v-if=\"coverUrl && !coverFailed\"\n class=\"source-card__cover\"\n :src=\"coverUrl\"\n @error=\"coverFailed = true\"\n />\n\n <div class=\"source-card__naming\">\n <p v-if=\"title\" class=\"source-card__title\">{{ title }}</p>\n <a\n v-if=\"link\"\n class=\"source-card__link\"\n :href=\"link\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n >\n {{ copy.sources.openSourceLabel }}\n <KitIcon name=\"external-link\" />\n </a>\n </div>\n </div>\n </div>\n\n <div v-if=\"expanded && references.length > 0\" class=\"source-card__body\">\n <p class=\"source-card__reference-heading\">{{ copy.sources.referenceLabel }}</p>\n <div v-for=\"(reference, refIndex) in references\" :key=\"refIndex\" class=\"source-card__reference\">\n <!-- A passage's own page image, enlargeable when there is one. -->\n <img\n v-if=\"reference.metadata?.screenshotUrl\"\n class=\"source-card__page-thumb\"\n :src=\"reference.metadata.screenshotUrl\"\n @click=\"viewPage(reference)\"\n />\n <div class=\"source-card__reference-text\">\n <p v-if=\"pageLineOf(reference)\" class=\"source-card__page-line\">\n {{ pageLineOf(reference) }}\n </p>\n <p v-if=\"passageOf(reference)\" class=\"source-card__passage\">{{ passageOf(reference) }}</p>\n </div>\n </div>\n </div>\n </div>\n</template>\n\n<style scoped>\n.source-card {\n position: relative;\n /* Named so the queries below measure this card, not the viewport. */\n container: source-card / inline-size;\n display: flex;\n flex-direction: column;\n gap: var(--spacing-4);\n padding: var(--spacing-6);\n border: 1px solid var(--card-stroke);\n border-radius: var(--r-md);\n background: var(--surface-raised);\n}\n\n.source-card__front {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-4);\n width: 100%;\n}\n\n/*\n * Stacked while the card is narrow and side by side once there is room. A\n * container query rather than a media query, because what matters is the\n * width of the card and the chat pane is often a column inside a wider page.\n */\n.source-card__details {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-5);\n}\n\n@container source-card (min-width: 420px) {\n .source-card__details {\n flex-direction: row;\n align-items: center;\n }\n}\n\n.source-card__toggle {\n position: absolute;\n top: var(--spacing-6);\n right: var(--spacing-6);\n display: grid;\n place-items: center;\n width: 32px;\n height: 32px;\n padding: 0;\n border: none;\n border-radius: var(--r-sm);\n background: none;\n color: var(--text-secondary);\n cursor: pointer;\n transition: transform 0.3s ease;\n}\n\n.source-card__toggle:hover {\n background: var(--surface-hover);\n color: var(--text-primary);\n}\n\n/* Points down while the card is shut and up once it is open. */\n.source-card__toggle--open {\n transform: rotate(180deg);\n}\n\n/* Whole and uncropped, since a cover is a page to be recognised rather than a\n texture to fill a box. */\n.source-card__cover {\n width: 150px;\n height: 150px;\n flex-shrink: 0;\n object-fit: contain;\n}\n\n.source-card__naming {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: var(--spacing-5);\n min-width: 0;\n}\n\n.source-card__heading {\n color: var(--text-helper);\n font-family: var(--font-label);\n font-size: var(--t-xs);\n font-weight: var(--fw-bold);\n text-transform: uppercase;\n}\n\n/* The document's name is the card's headline, so it carries the size. */\n.source-card__title {\n color: var(--text-primary);\n font-family: var(--font-display);\n font-size: var(--t-2xl);\n font-weight: var(--fw-regular);\n text-align: start;\n overflow-wrap: break-word;\n}\n\n.source-card__link {\n display: inline-flex;\n align-items: center;\n gap: var(--spacing-1);\n align-self: flex-start;\n color: var(--accent);\n font-family: var(--font-ui);\n font-size: var(--t-xs);\n font-weight: var(--fw-bold);\n}\n\n.source-card__body {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-4);\n padding-top: var(--spacing-4);\n border-top: 1px solid var(--card-stroke);\n}\n\n.source-card__reference-heading {\n color: var(--text-helper);\n font-family: var(--font-label);\n font-size: var(--t-xs);\n font-weight: var(--fw-bold);\n text-transform: uppercase;\n}\n\n/* Stacks and unstacks with the card, the same way the front does. */\n.source-card__reference {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-5);\n width: 100%;\n text-align: left;\n}\n\n@container source-card (min-width: 420px) {\n .source-card__reference {\n flex-direction: row;\n align-items: flex-start;\n }\n}\n\n.source-card__page-thumb {\n width: 150px;\n height: 150px;\n flex-shrink: 0;\n object-fit: contain;\n cursor: pointer;\n}\n\n.source-card__reference-text {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-4);\n width: 100%;\n min-width: 0;\n}\n\n.source-card__page-line {\n color: var(--text-primary);\n font-family: var(--font-label);\n font-size: var(--t-xs);\n font-weight: var(--fw-bold);\n text-transform: uppercase;\n letter-spacing: 0.12px;\n}\n\n.source-card__passage {\n color: var(--text-secondary);\n font-family: var(--font-body);\n font-size: var(--t-sm);\n line-height: var(--lh-body);\n overflow-wrap: break-word;\n}\n</style>\n","<script setup lang=\"ts\">\n/**\n * One document an answer drew on: a folded card with the document's name,\n * cover, and link on the front, and the cited passages inside. The fold keeps\n * a many-source answer readable; a reader opens the card they care about.\n */\nimport { computed, ref, watch } from 'vue';\n\nimport { formatCopy, useChatCopy } from '../copy';\nimport { decodeHtmlEntities, stripHtmlTags } from '../services/entities';\nimport type { SourceResult, UsedSource } from '../types';\nimport KitIcon from './KitIcon.vue';\n\nconst props = defineProps<{\n source: UsedSource;\n /** The card's position in the list, for its \"Source N\" heading. */\n index: number;\n}>();\n\nconst emit = defineEmits<{\n /** A passage thumbnail was clicked; the list decides how to show the page. */\n viewPage: [payload: { url: string; title: string; detail: string }];\n}>();\n\nconst copy = useChatCopy();\n\nconst expanded = ref(false);\n\nconst title = computed(() => decodeHtmlEntities(props.source.metadata?.title ?? ''));\nconst link = computed(() => props.source.metadata?.link || null);\n\n// Older threads persisted the cover at the source's root; see UsedSource.\nconst coverUrl = computed(\n () => props.source.metadata?.screenshotUrl || props.source.screenshotUrl || '',\n);\n\n// Cover URLs are short-lived signed links; a broken image is worse than none.\nconst coverFailed = ref(false);\nwatch(coverUrl, () => {\n coverFailed.value = false;\n});\n\n/** Retrieval hits with something to show; an empty hit would render a blank row. */\nconst references = computed(() =>\n props.source.results.filter((result) => result.content?.trim() || result.metadata?.screenshotUrl),\n);\n\nfunction passageOf(result: SourceResult): string {\n // Stripped before decoding, so an author's own \"<p>\" survives as text.\n return decodeHtmlEntities(stripHtmlTags(result.content ?? '')).trim();\n}\n\n/** \"Page 12\" or \"Page 12, Text page 10\", from the copy templates. */\nfunction pageLineOf(result: SourceResult): string {\n const { page, textPage } = result.metadata ?? {};\n return [\n page !== undefined ? formatCopy(copy.sources.pageLabel, { page }) : '',\n textPage !== undefined ? formatCopy(copy.sources.textPageLabel, { textPage }) : '',\n ]\n .filter(Boolean)\n .join(', ');\n}\n\nfunction viewPage(result: SourceResult): void {\n const url = result.metadata?.screenshotUrl;\n if (!url) return;\n emit('viewPage', {\n url,\n title: title.value || formatCopy(copy.sources.sourceLabel, { n: props.index + 1 }),\n detail: pageLineOf(result),\n });\n}\n</script>\n\n<template>\n <div class=\"source-card\">\n <!-- Sits over the top right corner rather than in the flow, so the\n document's name starts at the card's edge and the control stays put\n wherever the name wraps to. -->\n <button\n type=\"button\"\n class=\"source-card__toggle\"\n :class=\"{ 'source-card__toggle--open': expanded }\"\n :aria-expanded=\"expanded\"\n :aria-label=\"expanded ? copy.sources.collapseLabel : copy.sources.expandLabel\"\n @click=\"expanded = !expanded\"\n >\n <KitIcon name=\"chevron-down\" />\n </button>\n\n <div class=\"source-card__front\">\n <p class=\"source-card__heading\">\n {{ formatCopy(copy.sources.sourceLabel, { n: index + 1 }) }}\n </p>\n\n <div class=\"source-card__details\">\n <img\n v-if=\"coverUrl && !coverFailed\"\n class=\"source-card__cover\"\n :src=\"coverUrl\"\n @error=\"coverFailed = true\"\n />\n\n <div class=\"source-card__naming\">\n <p v-if=\"title\" class=\"source-card__title\">{{ title }}</p>\n <a\n v-if=\"link\"\n class=\"source-card__link\"\n :href=\"link\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n >\n {{ copy.sources.openSourceLabel }}\n <KitIcon name=\"external-link\" />\n </a>\n </div>\n </div>\n </div>\n\n <div v-if=\"expanded && references.length > 0\" class=\"source-card__body\">\n <p class=\"source-card__reference-heading\">{{ copy.sources.referenceLabel }}</p>\n <div v-for=\"(reference, refIndex) in references\" :key=\"refIndex\" class=\"source-card__reference\">\n <!-- A passage's own page image, enlargeable when there is one. -->\n <img\n v-if=\"reference.metadata?.screenshotUrl\"\n class=\"source-card__page-thumb\"\n :src=\"reference.metadata.screenshotUrl\"\n @click=\"viewPage(reference)\"\n />\n <div class=\"source-card__reference-text\">\n <p v-if=\"pageLineOf(reference)\" class=\"source-card__page-line\">\n {{ pageLineOf(reference) }}\n </p>\n <p v-if=\"passageOf(reference)\" class=\"source-card__passage\">{{ passageOf(reference) }}</p>\n </div>\n </div>\n </div>\n </div>\n</template>\n\n<style scoped>\n.source-card {\n position: relative;\n /* Named so the queries below measure this card, not the viewport. */\n container: source-card / inline-size;\n display: flex;\n flex-direction: column;\n gap: var(--spacing-4);\n padding: var(--spacing-6);\n border: 1px solid var(--card-stroke);\n border-radius: var(--r-md);\n background: var(--surface-raised);\n}\n\n.source-card__front {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-4);\n width: 100%;\n}\n\n/*\n * Stacked while the card is narrow and side by side once there is room. A\n * container query rather than a media query, because what matters is the\n * width of the card and the chat pane is often a column inside a wider page.\n */\n.source-card__details {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-5);\n}\n\n@container source-card (min-width: 420px) {\n .source-card__details {\n flex-direction: row;\n align-items: center;\n }\n}\n\n.source-card__toggle {\n position: absolute;\n top: var(--spacing-6);\n right: var(--spacing-6);\n display: grid;\n place-items: center;\n width: 32px;\n height: 32px;\n padding: 0;\n border: none;\n border-radius: var(--r-sm);\n background: none;\n color: var(--text-secondary);\n cursor: pointer;\n transition: transform 0.3s ease;\n}\n\n.source-card__toggle:hover {\n background: var(--surface-hover);\n color: var(--text-primary);\n}\n\n/* Points down while the card is shut and up once it is open. */\n.source-card__toggle--open {\n transform: rotate(180deg);\n}\n\n/* Whole and uncropped, since a cover is a page to be recognised rather than a\n texture to fill a box. */\n.source-card__cover {\n width: 150px;\n height: 150px;\n flex-shrink: 0;\n object-fit: contain;\n}\n\n.source-card__naming {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: var(--spacing-5);\n min-width: 0;\n}\n\n.source-card__heading {\n color: var(--text-helper);\n font-family: var(--font-label);\n font-size: var(--t-xs);\n font-weight: var(--fw-bold);\n text-transform: uppercase;\n}\n\n/* The document's name is the card's headline, so it carries the size. */\n.source-card__title {\n color: var(--text-primary);\n font-family: var(--font-display);\n font-size: var(--t-2xl);\n font-weight: var(--fw-regular);\n text-align: start;\n overflow-wrap: break-word;\n}\n\n.source-card__link {\n display: inline-flex;\n align-items: center;\n gap: var(--spacing-1);\n align-self: flex-start;\n color: var(--accent);\n font-family: var(--font-ui);\n font-size: var(--t-xs);\n font-weight: var(--fw-bold);\n}\n\n.source-card__body {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-4);\n padding-top: var(--spacing-4);\n border-top: 1px solid var(--card-stroke);\n}\n\n.source-card__reference-heading {\n color: var(--text-helper);\n font-family: var(--font-label);\n font-size: var(--t-xs);\n font-weight: var(--fw-bold);\n text-transform: uppercase;\n}\n\n/* Stacks and unstacks with the card, the same way the front does. */\n.source-card__reference {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-5);\n width: 100%;\n text-align: left;\n}\n\n@container source-card (min-width: 420px) {\n .source-card__reference {\n flex-direction: row;\n align-items: flex-start;\n }\n}\n\n.source-card__page-thumb {\n width: 150px;\n height: 150px;\n flex-shrink: 0;\n object-fit: contain;\n cursor: pointer;\n}\n\n.source-card__reference-text {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-4);\n width: 100%;\n min-width: 0;\n}\n\n.source-card__page-line {\n color: var(--text-primary);\n font-family: var(--font-label);\n font-size: var(--t-xs);\n font-weight: var(--fw-bold);\n text-transform: uppercase;\n letter-spacing: 0.12px;\n}\n\n.source-card__passage {\n color: var(--text-secondary);\n font-family: var(--font-body);\n font-size: var(--t-sm);\n line-height: var(--lh-body);\n overflow-wrap: break-word;\n}\n</style>\n","<script setup lang=\"ts\">\n/**\n * The documents an answer drew on, as folded cards underneath it. This is the\n * 'list' presentation of sources; the 'inline' presentation uses citation\n * chips and popovers instead.\n *\n * One page-image dialog is shared by all the cards, so two thumbnails can\n * never stack two dialogs.\n */\nimport { ref } from 'vue';\n\nimport { useChatCopy } from '../copy';\nimport type { UsedSource } from '../types';\nimport PageImageModal from './PageImageModal.vue';\nimport SourceCard from './SourceCard.vue';\n\ndefineProps<{ sources: UsedSource[] }>();\n\nconst copy = useChatCopy();\n\nconst openPage = ref<{ url: string; title: string; detail: string } | null>(null);\n</script>\n\n<template>\n <div v-if=\"sources.length > 0\" class=\"source-list\">\n <p v-if=\"copy.sources.introLine\" class=\"source-list__intro\">{{ copy.sources.introLine }}</p>\n\n <SourceCard\n v-for=\"(source, index) in sources\"\n :key=\"source.entityId ?? index\"\n :source=\"source\"\n :index=\"index\"\n @view-page=\"(payload) => (openPage = payload)\"\n />\n\n <p v-if=\"copy.sources.noticeLine\" class=\"source-list__notice\">{{ copy.sources.noticeLine }}</p>\n\n <PageImageModal\n v-if=\"openPage\"\n :url=\"openPage.url\"\n :title=\"openPage.title\"\n :detail=\"openPage.detail\"\n @close=\"openPage = null\"\n />\n </div>\n</template>\n\n<style scoped>\n.source-list {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-3);\n}\n\n.source-list__intro {\n color: var(--text-primary);\n font-family: var(--font-body);\n font-size: var(--t-sm);\n font-weight: var(--fw-bold);\n}\n\n.source-list__notice {\n color: var(--text-helper);\n font-family: var(--font-body);\n font-size: var(--t-xs);\n line-height: var(--lh-body);\n /* The notice copy is client-locked down to its spacing, so consecutive\n spaces must survive rendering instead of collapsing. */\n white-space: pre-wrap;\n}\n</style>\n","<script setup lang=\"ts\">\n/**\n * The documents an answer drew on, as folded cards underneath it. This is the\n * 'list' presentation of sources; the 'inline' presentation uses citation\n * chips and popovers instead.\n *\n * One page-image dialog is shared by all the cards, so two thumbnails can\n * never stack two dialogs.\n */\nimport { ref } from 'vue';\n\nimport { useChatCopy } from '../copy';\nimport type { UsedSource } from '../types';\nimport PageImageModal from './PageImageModal.vue';\nimport SourceCard from './SourceCard.vue';\n\ndefineProps<{ sources: UsedSource[] }>();\n\nconst copy = useChatCopy();\n\nconst openPage = ref<{ url: string; title: string; detail: string } | null>(null);\n</script>\n\n<template>\n <div v-if=\"sources.length > 0\" class=\"source-list\">\n <p v-if=\"copy.sources.introLine\" class=\"source-list__intro\">{{ copy.sources.introLine }}</p>\n\n <SourceCard\n v-for=\"(source, index) in sources\"\n :key=\"source.entityId ?? index\"\n :source=\"source\"\n :index=\"index\"\n @view-page=\"(payload) => (openPage = payload)\"\n />\n\n <p v-if=\"copy.sources.noticeLine\" class=\"source-list__notice\">{{ copy.sources.noticeLine }}</p>\n\n <PageImageModal\n v-if=\"openPage\"\n :url=\"openPage.url\"\n :title=\"openPage.title\"\n :detail=\"openPage.detail\"\n @close=\"openPage = null\"\n />\n </div>\n</template>\n\n<style scoped>\n.source-list {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-3);\n}\n\n.source-list__intro {\n color: var(--text-primary);\n font-family: var(--font-body);\n font-size: var(--t-sm);\n font-weight: var(--fw-bold);\n}\n\n.source-list__notice {\n color: var(--text-helper);\n font-family: var(--font-body);\n font-size: var(--t-xs);\n line-height: var(--lh-body);\n /* The notice copy is client-locked down to its spacing, so consecutive\n spaces must survive rendering instead of collapsing. */\n white-space: pre-wrap;\n}\n</style>\n","<script setup lang=\"ts\">\n/**\n * The waiting animation on its own, with no wording around it.\n *\n * Used twice: ThinkingIndicator pairs it with a label while an answer is\n * being retrieved, and the assistant's own header shows it bare while the\n * answer is still being written.\n */\nimport type { ThinkingVariant } from '../types';\n\ndefineProps<{\n /** Which animation plays: the spinning ring (default), stars, or dots. */\n variant?: ThinkingVariant;\n}>();\n</script>\n\n<template>\n <!-- Three four-pointed stars growing and fading in a staggered loop. -->\n <span v-if=\"variant === 'sparkles'\" class=\"thinking__sparkles\">\n <svg v-for=\"n in 3\" :key=\"n\" class=\"thinking__sparkle\" viewBox=\"0 0 16 16\">\n <path d=\"M8 0 Q9.4 6.6 16 8 Q9.4 9.4 8 16 Q6.6 9.4 0 8 Q6.6 6.6 8 0 Z\" fill=\"currentColor\" />\n </svg>\n </span>\n\n <!-- Three dots rising in turn, the familiar \"still typing\" shape. -->\n <span v-else-if=\"variant === 'dots'\" class=\"thinking__dots\">\n <span v-for=\"n in 3\" :key=\"n\" class=\"thinking__dot\" />\n </span>\n\n <span v-else class=\"thinking__spinner\" />\n</template>\n\n<style scoped>\n/*\n * How far around the ring the lit part reaches. Registered so it can be\n * animated; a plain custom property would jump between values instead of\n * sweeping through them.\n */\n@property --thinking-arc {\n syntax: '<angle>';\n initial-value: 121deg;\n inherits: false;\n}\n\n/*\n * A ring that fades away around its own circumference. The colour is one flat\n * fill; both the fade and the hollow middle are cut out of it by masks, which\n * keeps the fade a pure change in opacity instead of a slide toward grey.\n *\n * Two animations run together. The ring turns at a steady rate, while the lit\n * arc stretches and gathers, so the bright head pulls away from the faded tail\n * and the tail then catches back up.\n */\n.thinking__spinner {\n flex: none;\n width: 26px;\n height: 26px;\n border-radius: var(--r-pill);\n background: var(--accent);\n mask:\n conic-gradient(\n from 90deg,\n transparent 0deg,\n #000 var(--thinking-arc),\n transparent calc(var(--thinking-arc) + 1deg)\n ),\n radial-gradient(farthest-side, transparent calc(100% - 6px), #000 calc(100% - 6px));\n mask-composite: intersect;\n animation:\n thinking-spin 1s linear infinite,\n thinking-chase 1.8s ease-in-out infinite;\n}\n\n@keyframes thinking-spin {\n to {\n transform: rotate(1turn);\n }\n}\n\n/*\n * The tip's real speed is the ring's rotation plus however fast this arc is\n * growing, so the arc may never shrink faster than the ring turns or the tip\n * would track backwards. Gathering back in takes longer than stretching out,\n * and does so at a steady rate, which keeps the tip crawling forwards at its\n * slowest instead of reversing.\n */\n@keyframes thinking-chase {\n 0% {\n --thinking-arc: 40deg;\n animation-timing-function: ease-in-out;\n }\n 40% {\n --thinking-arc: 247deg;\n animation-timing-function: linear;\n }\n 100% {\n --thinking-arc: 40deg;\n }\n}\n\n.thinking__sparkles {\n position: relative;\n flex: none;\n width: 32px;\n height: 32px;\n color: var(--accent);\n}\n\n.thinking__dots {\n display: flex;\n flex: none;\n align-items: center;\n gap: var(--spacing-1);\n}\n\n.thinking__dot {\n width: 6px;\n height: 6px;\n border-radius: var(--r-pill);\n background: var(--accent);\n animation: thinking-dot 1.2s ease-in-out infinite both;\n}\n\n/* Staggered by a third of the cycle each, so the rise travels along the row\n rather than the three moving together. */\n.thinking__dot:nth-child(2) {\n animation-delay: 0.15s;\n}\n\n.thinking__dot:nth-child(3) {\n animation-delay: 0.3s;\n}\n\n@keyframes thinking-dot {\n 0%,\n /* Most of the cycle is spent at rest, so the row reads as a travelling\n rise instead of a constant wobble. */\n 60%,\n 100% {\n transform: translateY(0);\n opacity: 0.4;\n }\n\n 30% {\n transform: translateY(-4px);\n opacity: 1;\n }\n}\n\n/*\n * Every star rides the same diagonal, from the top left corner to the bottom\n * right: it appears small, grows to full size at the middle of its run, and\n * shrinks away as it leaves. The negative delays start each one a third of a\n * lap behind the last, so one star is always near full size while another\n * enters and another leaves. Each sits in its own lane across the diagonal,\n * so their paths never overlap.\n */\n.thinking__sparkle {\n position: absolute;\n /* Linear, so a star crosses the frame at one steady speed instead of\n rushing in, hanging in the middle, and rushing out. */\n animation: thinking-sparkle 1.5s linear infinite both;\n}\n\n/* The lanes sit roughly on one line, spaced so the visible gaps between the\n stars match. Equal center distances would not look even, because a bigger\n star's tips reach further toward its neighbour, so these positions started\n from gap-plus-reach math and were then tuned by eye. */\n.thinking__sparkle:nth-child(1) {\n top: 7px;\n left: 6px;\n width: 18px;\n height: 18px;\n}\n\n.thinking__sparkle:nth-child(2) {\n top: 1.7px;\n left: 19.3px;\n width: 11px;\n height: 11px;\n animation-delay: -0.5s;\n}\n\n.thinking__sparkle:nth-child(3) {\n top: 20.3px;\n left: 3.7px;\n width: 8px;\n height: 8px;\n animation-delay: -1s;\n}\n\n/* The middle keyframes hold scale at full through the center third of the\n run, while their translate values stay proportional to time so the linear\n travel never changes speed. */\n@keyframes thinking-sparkle {\n 0% {\n transform: translate(-9px, -9px) scale(0);\n opacity: 0;\n }\n 35% {\n transform: translate(-2.7px, -2.7px) scale(1);\n opacity: 1;\n }\n 65% {\n transform: translate(2.7px, 2.7px) scale(1);\n opacity: 1;\n }\n 100% {\n transform: translate(9px, 9px) scale(0);\n opacity: 0;\n }\n}\n</style>\n","<script setup lang=\"ts\">\n/**\n * The waiting animation on its own, with no wording around it.\n *\n * Used twice: ThinkingIndicator pairs it with a label while an answer is\n * being retrieved, and the assistant's own header shows it bare while the\n * answer is still being written.\n */\nimport type { ThinkingVariant } from '../types';\n\ndefineProps<{\n /** Which animation plays: the spinning ring (default), stars, or dots. */\n variant?: ThinkingVariant;\n}>();\n</script>\n\n<template>\n <!-- Three four-pointed stars growing and fading in a staggered loop. -->\n <span v-if=\"variant === 'sparkles'\" class=\"thinking__sparkles\">\n <svg v-for=\"n in 3\" :key=\"n\" class=\"thinking__sparkle\" viewBox=\"0 0 16 16\">\n <path d=\"M8 0 Q9.4 6.6 16 8 Q9.4 9.4 8 16 Q6.6 9.4 0 8 Q6.6 6.6 8 0 Z\" fill=\"currentColor\" />\n </svg>\n </span>\n\n <!-- Three dots rising in turn, the familiar \"still typing\" shape. -->\n <span v-else-if=\"variant === 'dots'\" class=\"thinking__dots\">\n <span v-for=\"n in 3\" :key=\"n\" class=\"thinking__dot\" />\n </span>\n\n <span v-else class=\"thinking__spinner\" />\n</template>\n\n<style scoped>\n/*\n * How far around the ring the lit part reaches. Registered so it can be\n * animated; a plain custom property would jump between values instead of\n * sweeping through them.\n */\n@property --thinking-arc {\n syntax: '<angle>';\n initial-value: 121deg;\n inherits: false;\n}\n\n/*\n * A ring that fades away around its own circumference. The colour is one flat\n * fill; both the fade and the hollow middle are cut out of it by masks, which\n * keeps the fade a pure change in opacity instead of a slide toward grey.\n *\n * Two animations run together. The ring turns at a steady rate, while the lit\n * arc stretches and gathers, so the bright head pulls away from the faded tail\n * and the tail then catches back up.\n */\n.thinking__spinner {\n flex: none;\n width: 26px;\n height: 26px;\n border-radius: var(--r-pill);\n background: var(--accent);\n mask:\n conic-gradient(\n from 90deg,\n transparent 0deg,\n #000 var(--thinking-arc),\n transparent calc(var(--thinking-arc) + 1deg)\n ),\n radial-gradient(farthest-side, transparent calc(100% - 6px), #000 calc(100% - 6px));\n mask-composite: intersect;\n animation:\n thinking-spin 1s linear infinite,\n thinking-chase 1.8s ease-in-out infinite;\n}\n\n@keyframes thinking-spin {\n to {\n transform: rotate(1turn);\n }\n}\n\n/*\n * The tip's real speed is the ring's rotation plus however fast this arc is\n * growing, so the arc may never shrink faster than the ring turns or the tip\n * would track backwards. Gathering back in takes longer than stretching out,\n * and does so at a steady rate, which keeps the tip crawling forwards at its\n * slowest instead of reversing.\n */\n@keyframes thinking-chase {\n 0% {\n --thinking-arc: 40deg;\n animation-timing-function: ease-in-out;\n }\n 40% {\n --thinking-arc: 247deg;\n animation-timing-function: linear;\n }\n 100% {\n --thinking-arc: 40deg;\n }\n}\n\n.thinking__sparkles {\n position: relative;\n flex: none;\n width: 32px;\n height: 32px;\n color: var(--accent);\n}\n\n.thinking__dots {\n display: flex;\n flex: none;\n align-items: center;\n gap: var(--spacing-1);\n}\n\n.thinking__dot {\n width: 6px;\n height: 6px;\n border-radius: var(--r-pill);\n background: var(--accent);\n animation: thinking-dot 1.2s ease-in-out infinite both;\n}\n\n/* Staggered by a third of the cycle each, so the rise travels along the row\n rather than the three moving together. */\n.thinking__dot:nth-child(2) {\n animation-delay: 0.15s;\n}\n\n.thinking__dot:nth-child(3) {\n animation-delay: 0.3s;\n}\n\n@keyframes thinking-dot {\n 0%,\n /* Most of the cycle is spent at rest, so the row reads as a travelling\n rise instead of a constant wobble. */\n 60%,\n 100% {\n transform: translateY(0);\n opacity: 0.4;\n }\n\n 30% {\n transform: translateY(-4px);\n opacity: 1;\n }\n}\n\n/*\n * Every star rides the same diagonal, from the top left corner to the bottom\n * right: it appears small, grows to full size at the middle of its run, and\n * shrinks away as it leaves. The negative delays start each one a third of a\n * lap behind the last, so one star is always near full size while another\n * enters and another leaves. Each sits in its own lane across the diagonal,\n * so their paths never overlap.\n */\n.thinking__sparkle {\n position: absolute;\n /* Linear, so a star crosses the frame at one steady speed instead of\n rushing in, hanging in the middle, and rushing out. */\n animation: thinking-sparkle 1.5s linear infinite both;\n}\n\n/* The lanes sit roughly on one line, spaced so the visible gaps between the\n stars match. Equal center distances would not look even, because a bigger\n star's tips reach further toward its neighbour, so these positions started\n from gap-plus-reach math and were then tuned by eye. */\n.thinking__sparkle:nth-child(1) {\n top: 7px;\n left: 6px;\n width: 18px;\n height: 18px;\n}\n\n.thinking__sparkle:nth-child(2) {\n top: 1.7px;\n left: 19.3px;\n width: 11px;\n height: 11px;\n animation-delay: -0.5s;\n}\n\n.thinking__sparkle:nth-child(3) {\n top: 20.3px;\n left: 3.7px;\n width: 8px;\n height: 8px;\n animation-delay: -1s;\n}\n\n/* The middle keyframes hold scale at full through the center third of the\n run, while their translate values stay proportional to time so the linear\n travel never changes speed. */\n@keyframes thinking-sparkle {\n 0% {\n transform: translate(-9px, -9px) scale(0);\n opacity: 0;\n }\n 35% {\n transform: translate(-2.7px, -2.7px) scale(1);\n opacity: 1;\n }\n 65% {\n transform: translate(2.7px, 2.7px) scale(1);\n opacity: 1;\n }\n 100% {\n transform: translate(9px, 9px) scale(0);\n opacity: 0;\n }\n}\n</style>\n","<script setup lang=\"ts\">\n/**\n * The model's reasoning behind an answer, folded behind a small toggle. Shown\n * in two places when the app turns on showThinking: live under the thinking\n * indicator while an answer is being retrieved, and on the finished reply.\n */\nimport { nextTick, ref, watch } from 'vue';\n\nimport { useChatCopy } from '../copy';\n\nconst props = defineProps<{\n text: string;\n /** True for the live panel, which starts open and follows the text as it grows. */\n streaming?: boolean;\n}>();\n\nconst copy = useChatCopy();\n\n// Open from the start while streaming, since that panel exists to be watched.\n// On a finished reply the reasoning is an aside, so it starts folded.\nconst open = ref(props.streaming === true);\nconst body = ref<HTMLElement | null>(null);\n\n// Keeps the newest reasoning in view while it streams, like a terminal tail.\nwatch(\n () => props.text,\n async () => {\n if (!props.streaming || !open.value) return;\n await nextTick();\n const el = body.value;\n if (el) el.scrollTop = el.scrollHeight;\n },\n);\n</script>\n\n<template>\n <div class=\"thinking-process\">\n <button type=\"button\" class=\"thinking-process__toggle\" @click=\"open = !open\">\n {{ open ? copy.reasoning.hideLabel : copy.reasoning.showLabel }}\n </button>\n <div v-if=\"open\" ref=\"body\" class=\"thinking-process__body\">{{ text }}</div>\n </div>\n</template>\n\n<style scoped>\n.thinking-process {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-2);\n width: 100%;\n}\n\n.thinking-process__toggle {\n align-self: flex-start;\n padding: 0;\n border: none;\n background: none;\n color: var(--text-helper);\n font-family: var(--font-label);\n font-size: var(--t-xs);\n font-weight: var(--fw-bold);\n text-transform: uppercase;\n cursor: pointer;\n}\n\n.thinking-process__toggle:hover {\n color: var(--text-primary);\n}\n\n.thinking-process__body {\n max-height: 220px;\n overflow-y: auto;\n padding: var(--spacing-3) var(--spacing-4);\n border-radius: var(--r-lg);\n background: var(--surface-raised);\n color: var(--text-secondary);\n font-family: var(--font-body);\n font-size: var(--t-sm);\n line-height: var(--lh-body);\n white-space: pre-wrap;\n overflow-wrap: break-word;\n scrollbar-width: thin;\n scrollbar-color: var(--scrollbar-thumb) transparent;\n}\n</style>\n","<script setup lang=\"ts\">\n/**\n * The model's reasoning behind an answer, folded behind a small toggle. Shown\n * in two places when the app turns on showThinking: live under the thinking\n * indicator while an answer is being retrieved, and on the finished reply.\n */\nimport { nextTick, ref, watch } from 'vue';\n\nimport { useChatCopy } from '../copy';\n\nconst props = defineProps<{\n text: string;\n /** True for the live panel, which starts open and follows the text as it grows. */\n streaming?: boolean;\n}>();\n\nconst copy = useChatCopy();\n\n// Open from the start while streaming, since that panel exists to be watched.\n// On a finished reply the reasoning is an aside, so it starts folded.\nconst open = ref(props.streaming === true);\nconst body = ref<HTMLElement | null>(null);\n\n// Keeps the newest reasoning in view while it streams, like a terminal tail.\nwatch(\n () => props.text,\n async () => {\n if (!props.streaming || !open.value) return;\n await nextTick();\n const el = body.value;\n if (el) el.scrollTop = el.scrollHeight;\n },\n);\n</script>\n\n<template>\n <div class=\"thinking-process\">\n <button type=\"button\" class=\"thinking-process__toggle\" @click=\"open = !open\">\n {{ open ? copy.reasoning.hideLabel : copy.reasoning.showLabel }}\n </button>\n <div v-if=\"open\" ref=\"body\" class=\"thinking-process__body\">{{ text }}</div>\n </div>\n</template>\n\n<style scoped>\n.thinking-process {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-2);\n width: 100%;\n}\n\n.thinking-process__toggle {\n align-self: flex-start;\n padding: 0;\n border: none;\n background: none;\n color: var(--text-helper);\n font-family: var(--font-label);\n font-size: var(--t-xs);\n font-weight: var(--fw-bold);\n text-transform: uppercase;\n cursor: pointer;\n}\n\n.thinking-process__toggle:hover {\n color: var(--text-primary);\n}\n\n.thinking-process__body {\n max-height: 220px;\n overflow-y: auto;\n padding: var(--spacing-3) var(--spacing-4);\n border-radius: var(--r-lg);\n background: var(--surface-raised);\n color: var(--text-secondary);\n font-family: var(--font-body);\n font-size: var(--t-sm);\n line-height: var(--lh-body);\n white-space: pre-wrap;\n overflow-wrap: break-word;\n scrollbar-width: thin;\n scrollbar-color: var(--scrollbar-thumb) transparent;\n}\n</style>\n","<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, ref, useSlots } from 'vue';\n\nimport { useChatCopy } from '../copy';\nimport { injectCitationChips } from '../services/citations';\nimport type { ChatEngineMessage } from '../composables/useChatEngine';\nimport type { ChatCitation, CitationType, MessageFeedback, ThinkingVariant } from '../types';\nimport CitationPopover from './CitationPopover.vue';\nimport FeedbackRow from './FeedbackRow.vue';\nimport IconButton from './IconButton.vue';\nimport KitIcon from './KitIcon.vue';\nimport SourceList from './SourceList.vue';\nimport ThinkingAnimation from './ThinkingAnimation.vue';\nimport ThinkingProcess from './ThinkingProcess.vue';\n\nconst props = defineProps<{\n message: ChatEngineMessage;\n citationType?: CitationType;\n /** Offers the reasoning behind this reply in a folded panel. */\n showThinking?: boolean;\n /** Which animation plays beside the header while this reply is still arriving. */\n thinkingVariant?: ThinkingVariant;\n /** Offers a button that copies the answer. Off by default. */\n showCopy?: boolean;\n}>();\n\nconst emit = defineEmits<{\n rate: [messageId: string, value: MessageFeedback];\n retry: [];\n citationClick: [messageId: string, citation: ChatCitation];\n}>();\n\nconst copy = useChatCopy();\nconst slots = useSlots();\n\n/**\n * The actions row exists once there is a finished answer to act on, and only\n * when something would go in it: an empty one still takes a gap from the\n * column above, so an app that wants no actions must get no row.\n *\n * Looser than showFeedback otherwise: copying a reply needs no server id, and\n * a reply the assistant declined is still worth copying.\n */\nfunction showActions(): boolean {\n const offersAnything = props.showCopy === true || Boolean(slots['message-actions']);\n return offersAnything && !props.message.isStreaming && Boolean(props.message.text);\n}\n\n// The button says \"copied\" for a moment, since nothing else visibly happens.\nconst copied = ref(false);\nlet copiedTimer: ReturnType<typeof setTimeout> | undefined;\n\nasync function copyAnswer(): Promise<void> {\n try {\n // The text as the model wrote it, not the rendered HTML: what a reader\n // pastes into a mail or a ticket should be the answer, not markup.\n await navigator.clipboard.writeText(props.message.text);\n copied.value = true;\n clearTimeout(copiedTimer);\n copiedTimer = setTimeout(() => {\n copied.value = false;\n }, 2000);\n } catch {\n // The browser can refuse clipboard access; the label just stays put.\n }\n}\n\nfunction showFeedback(): boolean {\n const m = props.message;\n return !m.isStreaming && !m.isOutOfScope && !m.failed && Boolean(m.basecampMessageId) && Boolean(m.text);\n}\n\n// The chips recompute as the html streams in and again when citations arrive.\n// While streaming, markers chip immediately as inert placeholders, because\n// the citations payload only lands after the text finishes.\nconst bodyHtml = computed(() => {\n if (props.citationType === 'list') return props.message.html;\n return injectCitationChips(props.message.html, props.message.citations, {\n pendingMarkers: props.message.isStreaming,\n });\n});\n\n// --- Citation popover ---\n\nconst openCitation = ref<ChatCitation | null>(null);\nconst popoverStyle = ref<Record<string, string>>({});\nconst body = ref<HTMLElement | null>(null);\n\n/** Matches the card's own width; needed here to keep it inside the viewport. */\nconst POPOVER_WIDTH = 350;\n/** Clearance kept from every screen edge. */\nconst VIEWPORT_MARGIN = 12;\n/** Gap between the chip and the card. */\nconst CHIP_GAP = 4;\n/** Height the card would like if the room is there. Passages vary wildly. */\nconst PREFERRED_HEIGHT = 280;\n/** Never squeeze it below this, even in a cramped viewport; it scrolls instead. */\nconst MIN_HEIGHT = 160;\n\n/**\n * Places the card against the chip in viewport coordinates. It is fixed and\n * teleported to the body rather than positioned inside the message, because\n * the conversation is a scrolling box: a card overhanging the last answer\n * would stretch that box and leave dead space under the chat.\n *\n * The card opens below the chip when there is room, flips above when there is\n * not, and is always capped to the space on whichever side it lands, so a long\n * passage scrolls inside the card instead of running off the screen.\n */\nfunction placeBy(chip: HTMLElement): Record<string, string> {\n const rect = chip.getBoundingClientRect();\n const width = Math.min(POPOVER_WIDTH, window.innerWidth - VIEWPORT_MARGIN * 2);\n\n // Right edge follows the chip, then comes back inside whichever screen edge it passed.\n const left = Math.min(\n Math.max(VIEWPORT_MARGIN, rect.right - width),\n window.innerWidth - width - VIEWPORT_MARGIN,\n );\n\n const roomBelow = window.innerHeight - rect.bottom - CHIP_GAP - VIEWPORT_MARGIN;\n const roomAbove = rect.top - CHIP_GAP - VIEWPORT_MARGIN;\n // Below by preference, but take the roomier side when below cannot fit.\n const openBelow = roomBelow >= PREFERRED_HEIGHT || roomBelow >= roomAbove;\n const maxHeight = Math.max(MIN_HEIGHT, openBelow ? roomBelow : roomAbove);\n\n return {\n left: `${left}px`,\n width: `${width}px`,\n maxHeight: `${maxHeight}px`,\n ...(openBelow\n ? { top: `${rect.bottom + CHIP_GAP}px` }\n : { bottom: `${window.innerHeight - rect.top + CHIP_GAP}px` }),\n };\n}\n\n/** Chips live inside v-html, so their clicks are caught here on the body. */\nfunction onBodyClick(event: MouseEvent | KeyboardEvent): void {\n const chip = (event.target as HTMLElement).closest?.('.citation-chip');\n if (!(chip instanceof HTMLElement) || !body.value) return;\n\n const citation = props.message.citations.find(\n (c) => c.ref === Number(chip.dataset.citationRef),\n );\n // Toggles: clicking the open citation's chip closes it.\n if (!citation || citation === openCitation.value) {\n openCitation.value = null;\n return;\n }\n popoverStyle.value = placeBy(chip);\n openCitation.value = citation;\n emit('citationClick', props.message.id, citation);\n}\n\nfunction onBodyKeydown(event: KeyboardEvent): void {\n if (event.key === 'Enter' || event.key === ' ') {\n onBodyClick(event);\n event.preventDefault();\n }\n}\n\n// A click anywhere else dismisses the popover.\nfunction onDocumentClick(event: MouseEvent): void {\n const target = event.target as HTMLElement;\n if (target.closest?.('.citation-chip') || target.closest?.('.citation-popover')) return;\n openCitation.value = null;\n}\nfunction closePopover(): void {\n openCitation.value = null;\n}\n\n/**\n * The card is fixed to the viewport, so it would drift away from its chip if\n * the conversation scrolled underneath it. Capture is needed to see the\n * message list scrolling; scrolling the card's own passage is exempt.\n */\nfunction onScroll(event: Event): void {\n const target = event.target;\n if (target instanceof HTMLElement && target.closest?.('.citation-popover')) return;\n closePopover();\n}\n\ndocument.addEventListener('click', onDocumentClick);\nwindow.addEventListener('scroll', onScroll, true);\nwindow.addEventListener('resize', closePopover);\nonBeforeUnmount(() => {\n document.removeEventListener('click', onDocumentClick);\n window.removeEventListener('scroll', onScroll, true);\n window.removeEventListener('resize', closePopover);\n clearTimeout(copiedTimer);\n});\n</script>\n\n<template>\n <div class=\"assistant-message\">\n <!-- Marked for the message list's auto-scroll: following stops rather\n than carrying this line off the top, so the answer can be read from\n its beginning. -->\n <div class=\"assistant-message__header\" data-reply-start>\n <span class=\"assistant-message__avatar\">\n <KitIcon name=\"sparkle\" />\n </span>\n <p class=\"assistant-message__name\">{{ copy.roles.assistant }}</p>\n <!-- Runs while the server is still sending, so the reader can tell more\n is coming without leaving the line they are reading. Stops once the\n stream closes and the citation popovers work, rather than waiting\n for the reveal to finish typing out text that has already arrived.\n The waiting indicator below the conversation has gone by now. -->\n <ThinkingAnimation v-if=\"message.isReceiving\" :variant=\"thinkingVariant\" />\n </div>\n\n <div v-if=\"message.isOutOfScope\" class=\"assistant-message__out-of-scope\">\n <KitIcon name=\"warning\" />\n <span>{{ copy.outOfScope.label }}</span>\n </div>\n\n <!-- Folded by default. The list's live panel has already closed by the\n time this bubble exists, since reasoning ends before the answer starts. -->\n <ThinkingProcess v-if=\"showThinking && message.thinking\" :text=\"message.thinking\" />\n\n <div class=\"assistant-message__body-wrap\">\n <!-- The engine sanitizes this HTML; the chips injected over it are built escaped. -->\n <!-- eslint-disable-next-line vue/no-v-html -->\n <div\n ref=\"body\"\n class=\"assistant-message__body\"\n v-html=\"bodyHtml\"\n @click=\"onBodyClick\"\n @keydown=\"onBodyKeydown\"\n />\n\n <Teleport to=\"body\">\n <CitationPopover\n v-if=\"openCitation\"\n class=\"assistant-message__popover\"\n :style=\"popoverStyle\"\n :citation=\"openCitation\"\n @close=\"openCitation = null\"\n />\n </Teleport>\n </div>\n\n <SourceList v-if=\"citationType === 'list' && !message.isStreaming\" :sources=\"message.sources\" />\n\n <div v-if=\"message.failed\" class=\"assistant-message__error\">\n <span>{{ copy.status.error }}</span>\n <button type=\"button\" class=\"assistant-message__retry\" @click=\"emit('retry')\">\n {{ copy.status.retryLabel }}\n </button>\n </div>\n\n <!-- One row: things you do with the answer on the left, rating it on the\n right. With no actions the rating is the only child and, being full\n width and right aligned, sits exactly where it did before this row\n existed. -->\n <div v-if=\"showActions() || showFeedback()\" class=\"assistant-message__footer\">\n <!-- The app's own actions come first, so a kit button never sits\n between two of theirs. The slot carries the message, since an\n action such as an export needs to know which answer it is for. -->\n <div v-if=\"showActions()\" class=\"assistant-message__actions\">\n <slot name=\"message-actions\" :message=\"message\" />\n\n <IconButton\n v-if=\"showCopy\"\n :label=\"copied ? copy.actions.copiedLabel : copy.actions.copyLabel\"\n class=\"assistant-message__copy\"\n @click=\"copyAnswer\"\n >\n <KitIcon name=\"copy\" />\n </IconButton>\n </div>\n\n <FeedbackRow\n v-if=\"showFeedback()\"\n :feedback=\"message.feedback\"\n @rate=\"(value) => emit('rate', message.id, value)\"\n />\n </div>\n </div>\n</template>\n\n<style scoped>\n.assistant-message {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-2);\n width: 100%;\n}\n\n.assistant-message__footer {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: var(--spacing-4);\n}\n\n/* Holds the app's own actions and the kit's copy button. Never shrinks, so\n the rating beside it gives up the width instead. */\n.assistant-message__actions {\n display: flex;\n flex: none;\n align-items: center;\n gap: var(--spacing-2);\n}\n\n.assistant-message__header {\n display: flex;\n align-items: center;\n gap: var(--spacing-4);\n}\n\n/* Accent disc holding the sparkle, marking the turn as the assistant's. */\n.assistant-message__avatar {\n display: grid;\n place-items: center;\n width: 32px;\n height: 32px;\n flex-shrink: 0;\n border-radius: var(--r-pill);\n background: var(--accent);\n color: var(--accent-fg);\n}\n\n.assistant-message__name {\n color: var(--text-primary);\n font-family: var(--font-body);\n font-size: var(--t-base);\n font-weight: var(--fw-bold);\n}\n\n.assistant-message__body-wrap {\n width: 100%;\n}\n\n/* Floats over the chat, including the input bar, from outside the scroll box. */\n.assistant-message__popover {\n position: fixed;\n z-index: 100;\n}\n\n.assistant-message__body :deep(.citation-chip) {\n display: inline-grid;\n place-items: center;\n min-width: 20px;\n height: 20px;\n padding: 0 5px;\n margin-left: var(--spacing-1);\n border-radius: var(--r-md);\n background: var(--citation-bg);\n color: var(--accent-fg);\n font-size: var(--t-xs);\n font-weight: var(--fw-bold);\n vertical-align: text-bottom;\n cursor: pointer;\n user-select: none;\n}\n\n.assistant-message__body :deep(.citation-chip:not(.citation-chip--pending):hover) {\n opacity: 0.7;\n}\n\n/* Waiting on its citation: holds the chip's place without claiming a number. */\n.assistant-message__body :deep(.citation-chip--pending) {\n cursor: default;\n opacity: 0.55;\n}\n\n.assistant-message__body {\n color: var(--text-primary);\n font-family: var(--font-body);\n font-size: var(--t-base);\n font-weight: var(--fw-regular);\n line-height: var(--lh-body);\n overflow-wrap: break-word;\n}\n\n/* Spacing between the markdown blocks the answer renders into. */\n.assistant-message__body :deep(p),\n.assistant-message__body :deep(ul),\n.assistant-message__body :deep(ol),\n.assistant-message__body :deep(pre),\n.assistant-message__body :deep(table),\n.assistant-message__body :deep(blockquote) {\n margin-block: 0 var(--spacing-4);\n}\n\n.assistant-message__body :deep(ul),\n.assistant-message__body :deep(ol) {\n padding-inline-start: var(--spacing-6);\n}\n\n.assistant-message__body :deep(a) {\n color: var(--accent);\n}\n\n/* Formulas, when the app turns math on. MathLive brings its own styling for\n the glyphs; these rules only place the span in the surrounding prose. */\n.assistant-message__body :deep(.chat-math) {\n /* A wide formula scrolls itself rather than stretching the answer. */\n max-width: 100%;\n overflow-x: auto;\n}\n\n.assistant-message__body :deep(.chat-math--display) {\n display: block;\n margin-block: var(--spacing-4);\n text-align: center;\n}\n\n.assistant-message__body :deep(code) {\n font-family: var(--font-mono);\n font-size: var(--t-sm);\n background: var(--surface-raised);\n padding: 1px 5px;\n border-radius: var(--r-sm);\n}\n\n.assistant-message__body :deep(pre) {\n background: var(--surface-raised);\n padding: var(--spacing-4);\n border-radius: var(--r-lg);\n overflow-x: auto;\n}\n\n/* Quoted passages and the note callouts the agent writes, marked by an accent bar. */\n.assistant-message__body :deep(blockquote) {\n padding: var(--spacing-3) var(--spacing-4);\n border-left: 3px solid var(--accent);\n border-radius: 0 var(--r-md) var(--r-md) 0;\n background: var(--surface-raised);\n}\n\n/* The shared block spacing would otherwise leave a gap inside the quote. */\n.assistant-message__body :deep(blockquote) > :last-child {\n margin-bottom: 0;\n}\n\n/* Ruled so a table reads as one, and scrolling on its own when it is wider\n than the chat column instead of stretching the answer. */\n.assistant-message__body :deep(table) {\n display: block;\n max-width: 100%;\n overflow-x: auto;\n border-collapse: collapse;\n}\n\n.assistant-message__body :deep(th),\n.assistant-message__body :deep(td) {\n padding: var(--spacing-2) var(--spacing-3);\n border: 1px solid var(--card-stroke);\n text-align: left;\n vertical-align: top;\n}\n\n.assistant-message__body :deep(th) {\n background: var(--surface-raised);\n font-weight: var(--fw-bold);\n}\n\n/* A bordered chip in the app's accent colour, sitting above the reply it marks. */\n.assistant-message__out-of-scope {\n display: flex;\n align-items: center;\n align-self: flex-start;\n gap: var(--spacing-2);\n padding: var(--spacing-3);\n border: 1px solid var(--accent);\n border-radius: var(--r-sm);\n background: color-mix(in srgb, var(--accent) 15%, transparent);\n color: var(--accent);\n font-family: var(--font-body);\n font-size: var(--t-xs);\n font-weight: var(--fw-bold);\n letter-spacing: 0.6px;\n text-transform: uppercase;\n}\n\n/* The reply sits further from the chip than the rest of the stack's spacing. */\n.assistant-message__out-of-scope + .assistant-message__body {\n margin-top: var(--spacing-2);\n}\n\n.assistant-message__error {\n display: flex;\n align-items: center;\n gap: var(--spacing-4);\n color: var(--warning);\n font-family: var(--font-body);\n font-size: var(--t-sm);\n}\n\n.assistant-message__retry {\n padding: var(--spacing-1) var(--spacing-3);\n border: 1px solid var(--card-stroke);\n border-radius: var(--r-md);\n background: none;\n color: var(--text-primary);\n font-family: var(--font-ui);\n font-size: var(--t-sm);\n font-weight: var(--fw-bold);\n cursor: pointer;\n}\n\n.assistant-message__retry:hover {\n background: var(--surface-hover);\n}\n</style>\n","<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, ref, useSlots } from 'vue';\n\nimport { useChatCopy } from '../copy';\nimport { injectCitationChips } from '../services/citations';\nimport type { ChatEngineMessage } from '../composables/useChatEngine';\nimport type { ChatCitation, CitationType, MessageFeedback, ThinkingVariant } from '../types';\nimport CitationPopover from './CitationPopover.vue';\nimport FeedbackRow from './FeedbackRow.vue';\nimport IconButton from './IconButton.vue';\nimport KitIcon from './KitIcon.vue';\nimport SourceList from './SourceList.vue';\nimport ThinkingAnimation from './ThinkingAnimation.vue';\nimport ThinkingProcess from './ThinkingProcess.vue';\n\nconst props = defineProps<{\n message: ChatEngineMessage;\n citationType?: CitationType;\n /** Offers the reasoning behind this reply in a folded panel. */\n showThinking?: boolean;\n /** Which animation plays beside the header while this reply is still arriving. */\n thinkingVariant?: ThinkingVariant;\n /** Offers a button that copies the answer. Off by default. */\n showCopy?: boolean;\n}>();\n\nconst emit = defineEmits<{\n rate: [messageId: string, value: MessageFeedback];\n retry: [];\n citationClick: [messageId: string, citation: ChatCitation];\n}>();\n\nconst copy = useChatCopy();\nconst slots = useSlots();\n\n/**\n * The actions row exists once there is a finished answer to act on, and only\n * when something would go in it: an empty one still takes a gap from the\n * column above, so an app that wants no actions must get no row.\n *\n * Looser than showFeedback otherwise: copying a reply needs no server id, and\n * a reply the assistant declined is still worth copying.\n */\nfunction showActions(): boolean {\n const offersAnything = props.showCopy === true || Boolean(slots['message-actions']);\n return offersAnything && !props.message.isStreaming && Boolean(props.message.text);\n}\n\n// The button says \"copied\" for a moment, since nothing else visibly happens.\nconst copied = ref(false);\nlet copiedTimer: ReturnType<typeof setTimeout> | undefined;\n\nasync function copyAnswer(): Promise<void> {\n try {\n // The text as the model wrote it, not the rendered HTML: what a reader\n // pastes into a mail or a ticket should be the answer, not markup.\n await navigator.clipboard.writeText(props.message.text);\n copied.value = true;\n clearTimeout(copiedTimer);\n copiedTimer = setTimeout(() => {\n copied.value = false;\n }, 2000);\n } catch {\n // The browser can refuse clipboard access; the label just stays put.\n }\n}\n\nfunction showFeedback(): boolean {\n const m = props.message;\n return !m.isStreaming && !m.isOutOfScope && !m.failed && Boolean(m.basecampMessageId) && Boolean(m.text);\n}\n\n// The chips recompute as the html streams in and again when citations arrive.\n// While streaming, markers chip immediately as inert placeholders, because\n// the citations payload only lands after the text finishes.\nconst bodyHtml = computed(() => {\n if (props.citationType === 'list') return props.message.html;\n return injectCitationChips(props.message.html, props.message.citations, {\n pendingMarkers: props.message.isStreaming,\n });\n});\n\n// --- Citation popover ---\n\nconst openCitation = ref<ChatCitation | null>(null);\nconst popoverStyle = ref<Record<string, string>>({});\nconst body = ref<HTMLElement | null>(null);\n\n/** Matches the card's own width; needed here to keep it inside the viewport. */\nconst POPOVER_WIDTH = 350;\n/** Clearance kept from every screen edge. */\nconst VIEWPORT_MARGIN = 12;\n/** Gap between the chip and the card. */\nconst CHIP_GAP = 4;\n/** Height the card would like if the room is there. Passages vary wildly. */\nconst PREFERRED_HEIGHT = 280;\n/** Never squeeze it below this, even in a cramped viewport; it scrolls instead. */\nconst MIN_HEIGHT = 160;\n\n/**\n * Places the card against the chip in viewport coordinates. It is fixed and\n * teleported to the body rather than positioned inside the message, because\n * the conversation is a scrolling box: a card overhanging the last answer\n * would stretch that box and leave dead space under the chat.\n *\n * The card opens below the chip when there is room, flips above when there is\n * not, and is always capped to the space on whichever side it lands, so a long\n * passage scrolls inside the card instead of running off the screen.\n */\nfunction placeBy(chip: HTMLElement): Record<string, string> {\n const rect = chip.getBoundingClientRect();\n const width = Math.min(POPOVER_WIDTH, window.innerWidth - VIEWPORT_MARGIN * 2);\n\n // Right edge follows the chip, then comes back inside whichever screen edge it passed.\n const left = Math.min(\n Math.max(VIEWPORT_MARGIN, rect.right - width),\n window.innerWidth - width - VIEWPORT_MARGIN,\n );\n\n const roomBelow = window.innerHeight - rect.bottom - CHIP_GAP - VIEWPORT_MARGIN;\n const roomAbove = rect.top - CHIP_GAP - VIEWPORT_MARGIN;\n // Below by preference, but take the roomier side when below cannot fit.\n const openBelow = roomBelow >= PREFERRED_HEIGHT || roomBelow >= roomAbove;\n const maxHeight = Math.max(MIN_HEIGHT, openBelow ? roomBelow : roomAbove);\n\n return {\n left: `${left}px`,\n width: `${width}px`,\n maxHeight: `${maxHeight}px`,\n ...(openBelow\n ? { top: `${rect.bottom + CHIP_GAP}px` }\n : { bottom: `${window.innerHeight - rect.top + CHIP_GAP}px` }),\n };\n}\n\n/** Chips live inside v-html, so their clicks are caught here on the body. */\nfunction onBodyClick(event: MouseEvent | KeyboardEvent): void {\n const chip = (event.target as HTMLElement).closest?.('.citation-chip');\n if (!(chip instanceof HTMLElement) || !body.value) return;\n\n const citation = props.message.citations.find(\n (c) => c.ref === Number(chip.dataset.citationRef),\n );\n // Toggles: clicking the open citation's chip closes it.\n if (!citation || citation === openCitation.value) {\n openCitation.value = null;\n return;\n }\n popoverStyle.value = placeBy(chip);\n openCitation.value = citation;\n emit('citationClick', props.message.id, citation);\n}\n\nfunction onBodyKeydown(event: KeyboardEvent): void {\n if (event.key === 'Enter' || event.key === ' ') {\n onBodyClick(event);\n event.preventDefault();\n }\n}\n\n// A click anywhere else dismisses the popover.\nfunction onDocumentClick(event: MouseEvent): void {\n const target = event.target as HTMLElement;\n if (target.closest?.('.citation-chip') || target.closest?.('.citation-popover')) return;\n openCitation.value = null;\n}\nfunction closePopover(): void {\n openCitation.value = null;\n}\n\n/**\n * The card is fixed to the viewport, so it would drift away from its chip if\n * the conversation scrolled underneath it. Capture is needed to see the\n * message list scrolling; scrolling the card's own passage is exempt.\n */\nfunction onScroll(event: Event): void {\n const target = event.target;\n if (target instanceof HTMLElement && target.closest?.('.citation-popover')) return;\n closePopover();\n}\n\ndocument.addEventListener('click', onDocumentClick);\nwindow.addEventListener('scroll', onScroll, true);\nwindow.addEventListener('resize', closePopover);\nonBeforeUnmount(() => {\n document.removeEventListener('click', onDocumentClick);\n window.removeEventListener('scroll', onScroll, true);\n window.removeEventListener('resize', closePopover);\n clearTimeout(copiedTimer);\n});\n</script>\n\n<template>\n <div class=\"assistant-message\">\n <!-- Marked for the message list's auto-scroll: following stops rather\n than carrying this line off the top, so the answer can be read from\n its beginning. -->\n <div class=\"assistant-message__header\" data-reply-start>\n <span class=\"assistant-message__avatar\">\n <KitIcon name=\"sparkle\" />\n </span>\n <p class=\"assistant-message__name\">{{ copy.roles.assistant }}</p>\n <!-- Runs while the server is still sending, so the reader can tell more\n is coming without leaving the line they are reading. Stops once the\n stream closes and the citation popovers work, rather than waiting\n for the reveal to finish typing out text that has already arrived.\n The waiting indicator below the conversation has gone by now. -->\n <ThinkingAnimation v-if=\"message.isReceiving\" :variant=\"thinkingVariant\" />\n </div>\n\n <div v-if=\"message.isOutOfScope\" class=\"assistant-message__out-of-scope\">\n <KitIcon name=\"warning\" />\n <span>{{ copy.outOfScope.label }}</span>\n </div>\n\n <!-- Folded by default. The list's live panel has already closed by the\n time this bubble exists, since reasoning ends before the answer starts. -->\n <ThinkingProcess v-if=\"showThinking && message.thinking\" :text=\"message.thinking\" />\n\n <div class=\"assistant-message__body-wrap\">\n <!-- The engine sanitizes this HTML; the chips injected over it are built escaped. -->\n <!-- eslint-disable-next-line vue/no-v-html -->\n <div\n ref=\"body\"\n class=\"assistant-message__body\"\n v-html=\"bodyHtml\"\n @click=\"onBodyClick\"\n @keydown=\"onBodyKeydown\"\n />\n\n <Teleport to=\"body\">\n <CitationPopover\n v-if=\"openCitation\"\n class=\"assistant-message__popover\"\n :style=\"popoverStyle\"\n :citation=\"openCitation\"\n @close=\"openCitation = null\"\n />\n </Teleport>\n </div>\n\n <SourceList v-if=\"citationType === 'list' && !message.isStreaming\" :sources=\"message.sources\" />\n\n <div v-if=\"message.failed\" class=\"assistant-message__error\">\n <span>{{ copy.status.error }}</span>\n <button type=\"button\" class=\"assistant-message__retry\" @click=\"emit('retry')\">\n {{ copy.status.retryLabel }}\n </button>\n </div>\n\n <!-- One row: things you do with the answer on the left, rating it on the\n right. With no actions the rating is the only child and, being full\n width and right aligned, sits exactly where it did before this row\n existed. -->\n <div v-if=\"showActions() || showFeedback()\" class=\"assistant-message__footer\">\n <!-- The app's own actions come first, so a kit button never sits\n between two of theirs. The slot carries the message, since an\n action such as an export needs to know which answer it is for. -->\n <div v-if=\"showActions()\" class=\"assistant-message__actions\">\n <slot name=\"message-actions\" :message=\"message\" />\n\n <IconButton\n v-if=\"showCopy\"\n :label=\"copied ? copy.actions.copiedLabel : copy.actions.copyLabel\"\n class=\"assistant-message__copy\"\n @click=\"copyAnswer\"\n >\n <KitIcon name=\"copy\" />\n </IconButton>\n </div>\n\n <FeedbackRow\n v-if=\"showFeedback()\"\n :feedback=\"message.feedback\"\n @rate=\"(value) => emit('rate', message.id, value)\"\n />\n </div>\n </div>\n</template>\n\n<style scoped>\n.assistant-message {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-2);\n width: 100%;\n}\n\n.assistant-message__footer {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: var(--spacing-4);\n}\n\n/* Holds the app's own actions and the kit's copy button. Never shrinks, so\n the rating beside it gives up the width instead. */\n.assistant-message__actions {\n display: flex;\n flex: none;\n align-items: center;\n gap: var(--spacing-2);\n}\n\n.assistant-message__header {\n display: flex;\n align-items: center;\n gap: var(--spacing-4);\n}\n\n/* Accent disc holding the sparkle, marking the turn as the assistant's. */\n.assistant-message__avatar {\n display: grid;\n place-items: center;\n width: 32px;\n height: 32px;\n flex-shrink: 0;\n border-radius: var(--r-pill);\n background: var(--accent);\n color: var(--accent-fg);\n}\n\n.assistant-message__name {\n color: var(--text-primary);\n font-family: var(--font-body);\n font-size: var(--t-base);\n font-weight: var(--fw-bold);\n}\n\n.assistant-message__body-wrap {\n width: 100%;\n}\n\n/* Floats over the chat, including the input bar, from outside the scroll box. */\n.assistant-message__popover {\n position: fixed;\n z-index: 100;\n}\n\n.assistant-message__body :deep(.citation-chip) {\n display: inline-grid;\n place-items: center;\n min-width: 20px;\n height: 20px;\n padding: 0 5px;\n margin-left: var(--spacing-1);\n border-radius: var(--r-md);\n background: var(--citation-bg);\n color: var(--accent-fg);\n font-size: var(--t-xs);\n font-weight: var(--fw-bold);\n vertical-align: text-bottom;\n cursor: pointer;\n user-select: none;\n}\n\n.assistant-message__body :deep(.citation-chip:not(.citation-chip--pending):hover) {\n opacity: 0.7;\n}\n\n/* Waiting on its citation: holds the chip's place without claiming a number. */\n.assistant-message__body :deep(.citation-chip--pending) {\n cursor: default;\n opacity: 0.55;\n}\n\n.assistant-message__body {\n color: var(--text-primary);\n font-family: var(--font-body);\n font-size: var(--t-base);\n font-weight: var(--fw-regular);\n line-height: var(--lh-body);\n overflow-wrap: break-word;\n}\n\n/* Spacing between the markdown blocks the answer renders into. */\n.assistant-message__body :deep(p),\n.assistant-message__body :deep(ul),\n.assistant-message__body :deep(ol),\n.assistant-message__body :deep(pre),\n.assistant-message__body :deep(table),\n.assistant-message__body :deep(blockquote) {\n margin-block: 0 var(--spacing-4);\n}\n\n.assistant-message__body :deep(ul),\n.assistant-message__body :deep(ol) {\n padding-inline-start: var(--spacing-6);\n}\n\n.assistant-message__body :deep(a) {\n color: var(--accent);\n}\n\n/* Formulas, when the app turns math on. MathLive brings its own styling for\n the glyphs; these rules only place the span in the surrounding prose. */\n.assistant-message__body :deep(.chat-math) {\n /* A wide formula scrolls itself rather than stretching the answer. */\n max-width: 100%;\n overflow-x: auto;\n}\n\n.assistant-message__body :deep(.chat-math--display) {\n display: block;\n margin-block: var(--spacing-4);\n text-align: center;\n}\n\n.assistant-message__body :deep(code) {\n font-family: var(--font-mono);\n font-size: var(--t-sm);\n background: var(--surface-raised);\n padding: 1px 5px;\n border-radius: var(--r-sm);\n}\n\n.assistant-message__body :deep(pre) {\n background: var(--surface-raised);\n padding: var(--spacing-4);\n border-radius: var(--r-lg);\n overflow-x: auto;\n}\n\n/* Quoted passages and the note callouts the agent writes, marked by an accent bar. */\n.assistant-message__body :deep(blockquote) {\n padding: var(--spacing-3) var(--spacing-4);\n border-left: 3px solid var(--accent);\n border-radius: 0 var(--r-md) var(--r-md) 0;\n background: var(--surface-raised);\n}\n\n/* The shared block spacing would otherwise leave a gap inside the quote. */\n.assistant-message__body :deep(blockquote) > :last-child {\n margin-bottom: 0;\n}\n\n/* Ruled so a table reads as one, and scrolling on its own when it is wider\n than the chat column instead of stretching the answer. */\n.assistant-message__body :deep(table) {\n display: block;\n max-width: 100%;\n overflow-x: auto;\n border-collapse: collapse;\n}\n\n.assistant-message__body :deep(th),\n.assistant-message__body :deep(td) {\n padding: var(--spacing-2) var(--spacing-3);\n border: 1px solid var(--card-stroke);\n text-align: left;\n vertical-align: top;\n}\n\n.assistant-message__body :deep(th) {\n background: var(--surface-raised);\n font-weight: var(--fw-bold);\n}\n\n/* A bordered chip in the app's accent colour, sitting above the reply it marks. */\n.assistant-message__out-of-scope {\n display: flex;\n align-items: center;\n align-self: flex-start;\n gap: var(--spacing-2);\n padding: var(--spacing-3);\n border: 1px solid var(--accent);\n border-radius: var(--r-sm);\n background: color-mix(in srgb, var(--accent) 15%, transparent);\n color: var(--accent);\n font-family: var(--font-body);\n font-size: var(--t-xs);\n font-weight: var(--fw-bold);\n letter-spacing: 0.6px;\n text-transform: uppercase;\n}\n\n/* The reply sits further from the chip than the rest of the stack's spacing. */\n.assistant-message__out-of-scope + .assistant-message__body {\n margin-top: var(--spacing-2);\n}\n\n.assistant-message__error {\n display: flex;\n align-items: center;\n gap: var(--spacing-4);\n color: var(--warning);\n font-family: var(--font-body);\n font-size: var(--t-sm);\n}\n\n.assistant-message__retry {\n padding: var(--spacing-1) var(--spacing-3);\n border: 1px solid var(--card-stroke);\n border-radius: var(--r-md);\n background: none;\n color: var(--text-primary);\n font-family: var(--font-ui);\n font-size: var(--t-sm);\n font-weight: var(--fw-bold);\n cursor: pointer;\n}\n\n.assistant-message__retry:hover {\n background: var(--surface-hover);\n}\n</style>\n","<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, onMounted, ref } from 'vue';\n\nimport { useChatCopy } from '../copy';\nimport type { ThinkingVariant } from '../types';\nimport ThinkingAnimation from './ThinkingAnimation.vue';\n\nconst props = defineProps<{\n /** Which animation plays: the spinning ring (default), stars, or dots. */\n variant?: ThinkingVariant;\n /**\n * Steps through copy.status.thinkingSequence while waiting, instead of\n * holding the single `thinking` label.\n */\n rotateLabel?: boolean;\n}>();\n\nconst copy = useChatCopy();\n\n/** How long each line holds before the next one. */\nconst LINE_HOLD_MS = 2000;\n\n// An app can switch the sequence off by giving copy an empty array, which\n// lands here as no sequence at all and falls back to the static label.\nconst lines = computed(() => (props.rotateLabel ? copy.status.thinkingSequence : []));\n\nconst index = ref(0);\nconst label = computed(() => lines.value[index.value] ?? copy.status.thinking);\n\nlet timer: ReturnType<typeof setTimeout> | undefined;\n\n/**\n * Advances once through the lines and holds on the last, rather than looping.\n * A cycling sequence reads as though the request has stalled and restarted.\n *\n * This component only exists while an answer is being retrieved, so mount and\n * unmount are the whole lifecycle of the sequence.\n */\nonMounted(() => {\n if (lines.value.length < 2) return;\n const step = () => {\n if (index.value >= lines.value.length - 1) return;\n index.value += 1;\n timer = setTimeout(step, LINE_HOLD_MS);\n };\n timer = setTimeout(step, LINE_HOLD_MS);\n});\n\nonBeforeUnmount(() => clearTimeout(timer));\n</script>\n\n<template>\n <div class=\"thinking\">\n <!-- Empty copy hides the wording and leaves the animation on its own. -->\n <span v-if=\"label\" class=\"thinking__label\">{{ label }}</span>\n <ThinkingAnimation :variant=\"variant\" />\n </div>\n</template>\n\n<style scoped>\n.thinking {\n display: flex;\n align-items: center;\n gap: var(--spacing-4);\n color: var(--text-secondary);\n font-family: var(--font-body);\n font-size: var(--t-base);\n}\n</style>\n","<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, onMounted, ref } from 'vue';\n\nimport { useChatCopy } from '../copy';\nimport type { ThinkingVariant } from '../types';\nimport ThinkingAnimation from './ThinkingAnimation.vue';\n\nconst props = defineProps<{\n /** Which animation plays: the spinning ring (default), stars, or dots. */\n variant?: ThinkingVariant;\n /**\n * Steps through copy.status.thinkingSequence while waiting, instead of\n * holding the single `thinking` label.\n */\n rotateLabel?: boolean;\n}>();\n\nconst copy = useChatCopy();\n\n/** How long each line holds before the next one. */\nconst LINE_HOLD_MS = 2000;\n\n// An app can switch the sequence off by giving copy an empty array, which\n// lands here as no sequence at all and falls back to the static label.\nconst lines = computed(() => (props.rotateLabel ? copy.status.thinkingSequence : []));\n\nconst index = ref(0);\nconst label = computed(() => lines.value[index.value] ?? copy.status.thinking);\n\nlet timer: ReturnType<typeof setTimeout> | undefined;\n\n/**\n * Advances once through the lines and holds on the last, rather than looping.\n * A cycling sequence reads as though the request has stalled and restarted.\n *\n * This component only exists while an answer is being retrieved, so mount and\n * unmount are the whole lifecycle of the sequence.\n */\nonMounted(() => {\n if (lines.value.length < 2) return;\n const step = () => {\n if (index.value >= lines.value.length - 1) return;\n index.value += 1;\n timer = setTimeout(step, LINE_HOLD_MS);\n };\n timer = setTimeout(step, LINE_HOLD_MS);\n});\n\nonBeforeUnmount(() => clearTimeout(timer));\n</script>\n\n<template>\n <div class=\"thinking\">\n <!-- Empty copy hides the wording and leaves the animation on its own. -->\n <span v-if=\"label\" class=\"thinking__label\">{{ label }}</span>\n <ThinkingAnimation :variant=\"variant\" />\n </div>\n</template>\n\n<style scoped>\n.thinking {\n display: flex;\n align-items: center;\n gap: var(--spacing-4);\n color: var(--text-secondary);\n font-family: var(--font-body);\n font-size: var(--t-base);\n}\n</style>\n","<script setup lang=\"ts\">\nimport type { ChatEngineMessage } from '../composables/useChatEngine';\n\ndefineProps<{ message: ChatEngineMessage }>();\n</script>\n\n<template>\n <div class=\"user-message\">\n <div class=\"user-message__bubble\">{{ message.text }}</div>\n </div>\n</template>\n\n<style scoped>\n.user-message {\n display: flex;\n justify-content: flex-end;\n width: 100%;\n}\n\n.user-message__bubble {\n max-width: 70%;\n padding: var(--spacing-4);\n border-radius: var(--card-radius);\n background: var(--user-bubble-bg);\n color: var(--user-bubble-fg);\n font-family: var(--font-body);\n font-size: var(--t-base);\n font-weight: var(--fw-regular);\n line-height: normal;\n white-space: pre-wrap;\n overflow-wrap: break-word;\n}\n</style>\n","<script setup lang=\"ts\">\nimport type { ChatEngineMessage } from '../composables/useChatEngine';\n\ndefineProps<{ message: ChatEngineMessage }>();\n</script>\n\n<template>\n <div class=\"user-message\">\n <div class=\"user-message__bubble\">{{ message.text }}</div>\n </div>\n</template>\n\n<style scoped>\n.user-message {\n display: flex;\n justify-content: flex-end;\n width: 100%;\n}\n\n.user-message__bubble {\n max-width: 70%;\n padding: var(--spacing-4);\n border-radius: var(--card-radius);\n background: var(--user-bubble-bg);\n color: var(--user-bubble-fg);\n font-family: var(--font-body);\n font-size: var(--t-base);\n font-weight: var(--fw-regular);\n line-height: normal;\n white-space: pre-wrap;\n overflow-wrap: break-word;\n}\n</style>\n","<script setup lang=\"ts\">\nimport { ref, watch } from 'vue';\n\nimport { useChatCopy } from '../copy';\nimport useAutoScroll from '../composables/useAutoScroll';\nimport type { ChatEngine, ChatEngineMessage } from '../composables/useChatEngine';\nimport type { AutoScrollMode, CitationType, MessageFeedback, ThinkingVariant } from '../types';\nimport AssistantMessage from './AssistantMessage.vue';\nimport ThinkingIndicator from './ThinkingIndicator.vue';\nimport ThinkingProcess from './ThinkingProcess.vue';\nimport UserMessage from './UserMessage.vue';\n\nconst props = defineProps<{\n engine: ChatEngine;\n citationType?: CitationType;\n thinkingVariant?: ThinkingVariant;\n /** Steps the waiting label through the copy sequence; see ThinkingIndicator. */\n rotateThinkingLabel?: boolean;\n /** Renders the model's reasoning, live and on finished replies. */\n showThinking?: boolean;\n /** How far the conversation follows a streaming answer; see AutoScrollMode. */\n autoScroll?: AutoScrollMode;\n /** Offers a copy button under each finished answer. */\n showCopy?: boolean;\n}>();\n\nconst emit = defineEmits<{\n /** A thumb was tapped: the reply, and the rating now on it (null once cleared). */\n rated: [message: ChatEngineMessage, value: MessageFeedback | null];\n}>();\n\nconst copy = useChatCopy();\n\nconst scroller = ref<HTMLElement | null>(null);\n\nconst { glideToBottom, resetToFollowing } = useAutoScroll(scroller, {\n mode: () => props.autoScroll ?? 'reply',\n // The newest reply's header. Queried rather than held as a ref, because it\n // belongs to a component inside a v-for and reaching into one would mean\n // driving it from the outside.\n anchor: () => {\n // Only a reply that is the newest message anchors the view. Between a\n // question being sent and its reply starting, the last header belongs to\n // the PREVIOUS answer, and stopping against that would hold the view\n // where it was, with the just-sent question below the fold.\n if (props.engine.messages.value.at(-1)?.role !== 'assistant') return null;\n const headers = scroller.value?.querySelectorAll<HTMLElement>('[data-reply-start]');\n return headers?.[headers.length - 1] ?? null;\n },\n});\n\n// Sending a message means the user wants the bottom again, even if they had\n// scrolled up to reread.\nwatch(\n () => props.engine.messages.value.length,\n (length, previousLength) => {\n if (length > previousLength && props.engine.messages.value.at(-1)?.role === 'user') {\n resetToFollowing();\n }\n },\n);\n\nwatch(\n () => [\n props.engine.messages.value.length,\n props.engine.messages.value.at(-1)?.html,\n props.engine.status.value,\n ],\n () => glideToBottom(),\n);\n\nfunction rate(messageId: string, value: MessageFeedback) {\n const message = props.engine.messages.value.find((m) => m.id === messageId);\n if (!message) return;\n\n // The engine applies a tap to the message before it talks to the server, so\n // reading straight back gives the rating now in effect. Every tap it accepts\n // changes the value (a repeat clears it), so an unchanged value means the\n // engine declined the tap and there is nothing to report.\n const before = message.feedback;\n void props.engine.setFeedback(messageId, value);\n if (message.feedback !== before) emit('rated', message, message.feedback);\n}\n</script>\n\n<template>\n <div ref=\"scroller\" class=\"message-list\">\n <!-- The welcome area shows until the first message is sent. -->\n <div v-if=\"engine.messages.value.length === 0\" class=\"message-list__welcome\">\n <slot name=\"welcome\">\n <div class=\"message-list__welcome-fallback\">\n <p v-if=\"copy.welcome.eyebrow\" class=\"message-list__welcome-eyebrow\">\n {{ copy.welcome.eyebrow }}\n </p>\n <h2 class=\"message-list__welcome-heading\">{{ copy.welcome.heading }}</h2>\n <p class=\"message-list__welcome-body\">{{ copy.welcome.body }}</p>\n </div>\n </slot>\n </div>\n\n <template v-for=\"message in engine.messages.value\" :key=\"message.id\">\n <UserMessage v-if=\"message.role === 'user'\" :message=\"message\" />\n <AssistantMessage\n v-else\n :message=\"message\"\n :citation-type=\"citationType\"\n :show-thinking=\"showThinking\"\n :thinking-variant=\"thinkingVariant\"\n :show-copy=\"showCopy\"\n @rate=\"rate\"\n @retry=\"() => engine.retry()\"\n @citation-click=\"(id, citation) => engine.noteCitationClick(id, citation)\"\n >\n <!-- Only forwarded when the app filled it, so the row stays empty\n rather than holding an empty wrapper. -->\n <template v-if=\"$slots['message-actions']\" #message-actions=\"{ message: reply }\">\n <slot name=\"message-actions\" :message=\"reply\" />\n </template>\n </AssistantMessage>\n </template>\n\n <ThinkingIndicator\n v-if=\"engine.status.value === 'thinking'\"\n :variant=\"thinkingVariant\"\n :rotate-label=\"rotateThinkingLabel\"\n />\n\n <!-- Reasoning streamed ahead of the answer, while the reply bubble does not exist yet. -->\n <ThinkingProcess\n v-if=\"showThinking && engine.liveThinking.value\"\n :text=\"engine.liveThinking.value\"\n streaming\n />\n\n <div v-if=\"engine.error.value\" class=\"message-list__error\">\n <span>{{\n engine.errorKind.value === 'rate-limited' ? copy.status.rateLimited : copy.status.error\n }}</span>\n <button type=\"button\" class=\"message-list__retry\" @click=\"() => engine.retry()\">\n {{ copy.status.retryLabel }}\n </button>\n </div>\n </div>\n</template>\n\n<style scoped>\n.message-list {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-5);\n flex: 1;\n min-height: 0;\n overflow-y: auto;\n padding: var(--spacing-6);\n scrollbar-width: thin;\n scrollbar-color: var(--scrollbar-thumb) transparent;\n}\n\n.message-list::-webkit-scrollbar {\n width: 10px;\n}\n\n.message-list::-webkit-scrollbar-track {\n background: transparent;\n}\n\n.message-list::-webkit-scrollbar-thumb {\n background: var(--scrollbar-thumb);\n border-radius: var(--r-pill);\n border: 3px solid transparent;\n background-clip: padding-box;\n}\n\n.message-list::-webkit-scrollbar-thumb:hover {\n background: var(--scrollbar-thumb-hover);\n background-clip: padding-box;\n}\n\n.message-list__welcome-eyebrow {\n color: var(--accent);\n font-family: var(--font-label);\n font-size: var(--t-xs);\n font-weight: var(--fw-bold);\n text-transform: uppercase;\n margin-bottom: var(--spacing-3);\n}\n\n.message-list__welcome-heading {\n color: var(--text-primary);\n font-family: var(--font-display);\n font-size: var(--t-2xl);\n font-weight: var(--fw-bold);\n letter-spacing: var(--tr-tight);\n margin-bottom: var(--spacing-3);\n}\n\n.message-list__welcome-body {\n color: var(--text-primary);\n font-family: var(--font-body);\n font-size: var(--t-base);\n line-height: var(--lh-body);\n}\n\n.message-list__error {\n display: flex;\n align-items: center;\n gap: var(--spacing-4);\n color: var(--warning);\n font-family: var(--font-body);\n font-size: var(--t-sm);\n}\n\n.message-list__retry {\n padding: var(--spacing-1) var(--spacing-3);\n border: 1px solid var(--card-stroke);\n border-radius: var(--r-md);\n background: none;\n color: var(--text-primary);\n font-family: var(--font-ui);\n font-size: var(--t-sm);\n font-weight: var(--fw-bold);\n cursor: pointer;\n}\n\n.message-list__retry:hover {\n background: var(--surface-hover);\n}\n</style>\n","<script setup lang=\"ts\">\nimport { ref, watch } from 'vue';\n\nimport { useChatCopy } from '../copy';\nimport useAutoScroll from '../composables/useAutoScroll';\nimport type { ChatEngine, ChatEngineMessage } from '../composables/useChatEngine';\nimport type { AutoScrollMode, CitationType, MessageFeedback, ThinkingVariant } from '../types';\nimport AssistantMessage from './AssistantMessage.vue';\nimport ThinkingIndicator from './ThinkingIndicator.vue';\nimport ThinkingProcess from './ThinkingProcess.vue';\nimport UserMessage from './UserMessage.vue';\n\nconst props = defineProps<{\n engine: ChatEngine;\n citationType?: CitationType;\n thinkingVariant?: ThinkingVariant;\n /** Steps the waiting label through the copy sequence; see ThinkingIndicator. */\n rotateThinkingLabel?: boolean;\n /** Renders the model's reasoning, live and on finished replies. */\n showThinking?: boolean;\n /** How far the conversation follows a streaming answer; see AutoScrollMode. */\n autoScroll?: AutoScrollMode;\n /** Offers a copy button under each finished answer. */\n showCopy?: boolean;\n}>();\n\nconst emit = defineEmits<{\n /** A thumb was tapped: the reply, and the rating now on it (null once cleared). */\n rated: [message: ChatEngineMessage, value: MessageFeedback | null];\n}>();\n\nconst copy = useChatCopy();\n\nconst scroller = ref<HTMLElement | null>(null);\n\nconst { glideToBottom, resetToFollowing } = useAutoScroll(scroller, {\n mode: () => props.autoScroll ?? 'reply',\n // The newest reply's header. Queried rather than held as a ref, because it\n // belongs to a component inside a v-for and reaching into one would mean\n // driving it from the outside.\n anchor: () => {\n // Only a reply that is the newest message anchors the view. Between a\n // question being sent and its reply starting, the last header belongs to\n // the PREVIOUS answer, and stopping against that would hold the view\n // where it was, with the just-sent question below the fold.\n if (props.engine.messages.value.at(-1)?.role !== 'assistant') return null;\n const headers = scroller.value?.querySelectorAll<HTMLElement>('[data-reply-start]');\n return headers?.[headers.length - 1] ?? null;\n },\n});\n\n// Sending a message means the user wants the bottom again, even if they had\n// scrolled up to reread.\nwatch(\n () => props.engine.messages.value.length,\n (length, previousLength) => {\n if (length > previousLength && props.engine.messages.value.at(-1)?.role === 'user') {\n resetToFollowing();\n }\n },\n);\n\nwatch(\n () => [\n props.engine.messages.value.length,\n props.engine.messages.value.at(-1)?.html,\n props.engine.status.value,\n ],\n () => glideToBottom(),\n);\n\nfunction rate(messageId: string, value: MessageFeedback) {\n const message = props.engine.messages.value.find((m) => m.id === messageId);\n if (!message) return;\n\n // The engine applies a tap to the message before it talks to the server, so\n // reading straight back gives the rating now in effect. Every tap it accepts\n // changes the value (a repeat clears it), so an unchanged value means the\n // engine declined the tap and there is nothing to report.\n const before = message.feedback;\n void props.engine.setFeedback(messageId, value);\n if (message.feedback !== before) emit('rated', message, message.feedback);\n}\n</script>\n\n<template>\n <div ref=\"scroller\" class=\"message-list\">\n <!-- The welcome area shows until the first message is sent. -->\n <div v-if=\"engine.messages.value.length === 0\" class=\"message-list__welcome\">\n <slot name=\"welcome\">\n <div class=\"message-list__welcome-fallback\">\n <p v-if=\"copy.welcome.eyebrow\" class=\"message-list__welcome-eyebrow\">\n {{ copy.welcome.eyebrow }}\n </p>\n <h2 class=\"message-list__welcome-heading\">{{ copy.welcome.heading }}</h2>\n <p class=\"message-list__welcome-body\">{{ copy.welcome.body }}</p>\n </div>\n </slot>\n </div>\n\n <template v-for=\"message in engine.messages.value\" :key=\"message.id\">\n <UserMessage v-if=\"message.role === 'user'\" :message=\"message\" />\n <AssistantMessage\n v-else\n :message=\"message\"\n :citation-type=\"citationType\"\n :show-thinking=\"showThinking\"\n :thinking-variant=\"thinkingVariant\"\n :show-copy=\"showCopy\"\n @rate=\"rate\"\n @retry=\"() => engine.retry()\"\n @citation-click=\"(id, citation) => engine.noteCitationClick(id, citation)\"\n >\n <!-- Only forwarded when the app filled it, so the row stays empty\n rather than holding an empty wrapper. -->\n <template v-if=\"$slots['message-actions']\" #message-actions=\"{ message: reply }\">\n <slot name=\"message-actions\" :message=\"reply\" />\n </template>\n </AssistantMessage>\n </template>\n\n <ThinkingIndicator\n v-if=\"engine.status.value === 'thinking'\"\n :variant=\"thinkingVariant\"\n :rotate-label=\"rotateThinkingLabel\"\n />\n\n <!-- Reasoning streamed ahead of the answer, while the reply bubble does not exist yet. -->\n <ThinkingProcess\n v-if=\"showThinking && engine.liveThinking.value\"\n :text=\"engine.liveThinking.value\"\n streaming\n />\n\n <div v-if=\"engine.error.value\" class=\"message-list__error\">\n <span>{{\n engine.errorKind.value === 'rate-limited' ? copy.status.rateLimited : copy.status.error\n }}</span>\n <button type=\"button\" class=\"message-list__retry\" @click=\"() => engine.retry()\">\n {{ copy.status.retryLabel }}\n </button>\n </div>\n </div>\n</template>\n\n<style scoped>\n.message-list {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-5);\n flex: 1;\n min-height: 0;\n overflow-y: auto;\n padding: var(--spacing-6);\n scrollbar-width: thin;\n scrollbar-color: var(--scrollbar-thumb) transparent;\n}\n\n.message-list::-webkit-scrollbar {\n width: 10px;\n}\n\n.message-list::-webkit-scrollbar-track {\n background: transparent;\n}\n\n.message-list::-webkit-scrollbar-thumb {\n background: var(--scrollbar-thumb);\n border-radius: var(--r-pill);\n border: 3px solid transparent;\n background-clip: padding-box;\n}\n\n.message-list::-webkit-scrollbar-thumb:hover {\n background: var(--scrollbar-thumb-hover);\n background-clip: padding-box;\n}\n\n.message-list__welcome-eyebrow {\n color: var(--accent);\n font-family: var(--font-label);\n font-size: var(--t-xs);\n font-weight: var(--fw-bold);\n text-transform: uppercase;\n margin-bottom: var(--spacing-3);\n}\n\n.message-list__welcome-heading {\n color: var(--text-primary);\n font-family: var(--font-display);\n font-size: var(--t-2xl);\n font-weight: var(--fw-bold);\n letter-spacing: var(--tr-tight);\n margin-bottom: var(--spacing-3);\n}\n\n.message-list__welcome-body {\n color: var(--text-primary);\n font-family: var(--font-body);\n font-size: var(--t-base);\n line-height: var(--lh-body);\n}\n\n.message-list__error {\n display: flex;\n align-items: center;\n gap: var(--spacing-4);\n color: var(--warning);\n font-family: var(--font-body);\n font-size: var(--t-sm);\n}\n\n.message-list__retry {\n padding: var(--spacing-1) var(--spacing-3);\n border: 1px solid var(--card-stroke);\n border-radius: var(--r-md);\n background: none;\n color: var(--text-primary);\n font-family: var(--font-ui);\n font-size: var(--t-sm);\n font-weight: var(--fw-bold);\n cursor: pointer;\n}\n\n.message-list__retry:hover {\n background: var(--surface-hover);\n}\n</style>\n","<script setup lang=\"ts\">\n/**\n * The chat itself: the conversation on top, the question field at the bottom.\n * The app creates the engine (see useChatEngine) and hands it in, which also\n * lets the app drive the chat from outside, for example when a suggested\n * question is clicked.\n */\nimport { provideChatCopy, type ChatCopyInput } from '../copy';\nimport type { ChatEngine, ChatEngineMessage } from '../composables/useChatEngine';\nimport type { AutoScrollMode, CitationType, MessageFeedback, ThinkingVariant } from '../types';\nimport InputBar from './InputBar.vue';\nimport MessageList from './MessageList.vue';\n\nconst props = defineProps<{\n /**\n * The conversation to render, from useChatEngine. Required, and not a\n * setting: everything below is optional and defaults to the behaviour the\n * kit had before that setting existed.\n */\n engine: ChatEngine;\n /** All of the app's copy text. Anything missing falls back to its default. */\n copy?: ChatCopyInput;\n /** Offers voice dictation, in browsers with speech recognition. */\n showMic?: boolean;\n /**\n * How sources are presented: 'inline' turns [^N] markers into numbered\n * chips with a passage popover, 'list' shows the source documents under\n * the answer. Defaults to 'inline'.\n */\n citationType?: CitationType;\n /** Which thinking animation plays while retrieving: 'ring' (default) or 'sparkles'. */\n thinkingVariant?: ThinkingVariant;\n /**\n * Steps the waiting label through `copy.status.thinkingSequence` instead of\n * holding one line, so a long retrieval reads as progress. Off by default.\n */\n rotateThinkingLabel?: boolean;\n /**\n * Shows the model's reasoning in a collapsible panel: live while an answer\n * is being retrieved, and folded on the finished reply. Off by default.\n */\n showThinking?: boolean;\n /**\n * How far the conversation follows a streaming answer. 'reply' (the\n * default) stops once the answer's header reaches the top, so it can be\n * read from the beginning; 'bottom' keeps the newest text in view; 'off'\n * leaves the view to the reader.\n */\n autoScroll?: AutoScrollMode;\n /**\n * Offers a button under each finished answer that copies it. Off by\n * default, since a client may not want the action at all.\n */\n showCopy?: boolean;\n}>();\n\nconst emit = defineEmits<{\n /**\n * A reply was given a thumb. Fires as soon as it is tapped, with the rating\n * now in effect, or null when the tap cleared it. The kit has already saved\n * the thumb; this is for an app that wants to follow it with a form of its\n * own, asking for more than up or down.\n */\n rated: [message: ChatEngineMessage, value: MessageFeedback | null];\n}>();\n\nprovideChatCopy(props.copy);\n</script>\n\n<template>\n <div class=\"chat\">\n <MessageList\n :engine=\"engine\"\n :citation-type=\"citationType ?? 'inline'\"\n :thinking-variant=\"thinkingVariant\"\n :rotate-thinking-label=\"rotateThinkingLabel\"\n :show-thinking=\"showThinking\"\n :auto-scroll=\"autoScroll\"\n :show-copy=\"showCopy\"\n @rated=\"(message, value) => emit('rated', message, value)\"\n >\n <template v-if=\"$slots['message-actions']\" #message-actions=\"{ message }\">\n <slot name=\"message-actions\" :message=\"message\" />\n </template>\n\n <!-- Only forward the slot when the app filled it, show fallback otherwise. -->\n <template v-if=\"$slots.welcome\" #welcome>\n <slot name=\"welcome\" />\n </template>\n </MessageList>\n\n <InputBar\n :busy=\"engine.busy.value\"\n :show-mic=\"showMic\"\n @send=\"(text, mode) => engine.sendMessage(text, mode)\"\n @stop=\"() => engine.stopGeneration()\"\n />\n </div>\n</template>\n\n<style scoped>\n.chat {\n display: flex;\n flex-direction: column;\n flex: 1;\n min-height: 0;\n}\n</style>\n","<script setup lang=\"ts\">\n/**\n * The chat itself: the conversation on top, the question field at the bottom.\n * The app creates the engine (see useChatEngine) and hands it in, which also\n * lets the app drive the chat from outside, for example when a suggested\n * question is clicked.\n */\nimport { provideChatCopy, type ChatCopyInput } from '../copy';\nimport type { ChatEngine, ChatEngineMessage } from '../composables/useChatEngine';\nimport type { AutoScrollMode, CitationType, MessageFeedback, ThinkingVariant } from '../types';\nimport InputBar from './InputBar.vue';\nimport MessageList from './MessageList.vue';\n\nconst props = defineProps<{\n /**\n * The conversation to render, from useChatEngine. Required, and not a\n * setting: everything below is optional and defaults to the behaviour the\n * kit had before that setting existed.\n */\n engine: ChatEngine;\n /** All of the app's copy text. Anything missing falls back to its default. */\n copy?: ChatCopyInput;\n /** Offers voice dictation, in browsers with speech recognition. */\n showMic?: boolean;\n /**\n * How sources are presented: 'inline' turns [^N] markers into numbered\n * chips with a passage popover, 'list' shows the source documents under\n * the answer. Defaults to 'inline'.\n */\n citationType?: CitationType;\n /** Which thinking animation plays while retrieving: 'ring' (default) or 'sparkles'. */\n thinkingVariant?: ThinkingVariant;\n /**\n * Steps the waiting label through `copy.status.thinkingSequence` instead of\n * holding one line, so a long retrieval reads as progress. Off by default.\n */\n rotateThinkingLabel?: boolean;\n /**\n * Shows the model's reasoning in a collapsible panel: live while an answer\n * is being retrieved, and folded on the finished reply. Off by default.\n */\n showThinking?: boolean;\n /**\n * How far the conversation follows a streaming answer. 'reply' (the\n * default) stops once the answer's header reaches the top, so it can be\n * read from the beginning; 'bottom' keeps the newest text in view; 'off'\n * leaves the view to the reader.\n */\n autoScroll?: AutoScrollMode;\n /**\n * Offers a button under each finished answer that copies it. Off by\n * default, since a client may not want the action at all.\n */\n showCopy?: boolean;\n}>();\n\nconst emit = defineEmits<{\n /**\n * A reply was given a thumb. Fires as soon as it is tapped, with the rating\n * now in effect, or null when the tap cleared it. The kit has already saved\n * the thumb; this is for an app that wants to follow it with a form of its\n * own, asking for more than up or down.\n */\n rated: [message: ChatEngineMessage, value: MessageFeedback | null];\n}>();\n\nprovideChatCopy(props.copy);\n</script>\n\n<template>\n <div class=\"chat\">\n <MessageList\n :engine=\"engine\"\n :citation-type=\"citationType ?? 'inline'\"\n :thinking-variant=\"thinkingVariant\"\n :rotate-thinking-label=\"rotateThinkingLabel\"\n :show-thinking=\"showThinking\"\n :auto-scroll=\"autoScroll\"\n :show-copy=\"showCopy\"\n @rated=\"(message, value) => emit('rated', message, value)\"\n >\n <template v-if=\"$slots['message-actions']\" #message-actions=\"{ message }\">\n <slot name=\"message-actions\" :message=\"message\" />\n </template>\n\n <!-- Only forward the slot when the app filled it, show fallback otherwise. -->\n <template v-if=\"$slots.welcome\" #welcome>\n <slot name=\"welcome\" />\n </template>\n </MessageList>\n\n <InputBar\n :busy=\"engine.busy.value\"\n :show-mic=\"showMic\"\n @send=\"(text, mode) => engine.sendMessage(text, mode)\"\n @stop=\"() => engine.stopGeneration()\"\n />\n </div>\n</template>\n\n<style scoped>\n.chat {\n display: flex;\n flex-direction: column;\n flex: 1;\n min-height: 0;\n}\n</style>\n"],"mappings":"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"}
|