From 922172fc60a0cb23123e663025900351dd23745a Mon Sep 17 00:00:00 2001 From: James Magahern Date: Thu, 27 Aug 2026 19:30:37 -0700 Subject: [PATCH] Add dedicated quick question API --- docs/api/streaming-chat.md | 32 ++++++++++-- .../Sybil/Sources/Sybil/SybilAPIClient.swift | 29 +++++++++++ .../Sources/Sybil/SybilAPIClienting.swift | 4 ++ .../Sybil/Sources/Sybil/SybilViewModel.swift | 8 ++- .../Sybil/Tests/SybilTests/SybilTests.swift | 52 +++++++++++++------ server/src/llm/quick-question.ts | 30 +++++++++++ server/src/routes.ts | 34 +++++++++++- server/tests/quick-question.test.ts | 27 ++++++++++ web/src/App.tsx | 11 ++-- web/src/lib/api.ts | 49 ++++++++++++----- web/src/lib/quick-question.ts | 17 ++++++ web/tests/quick-question.test.mjs | 18 +++++++ web/tsconfig.tsbuildinfo | 2 +- 13 files changed, 269 insertions(+), 44 deletions(-) create mode 100644 server/src/llm/quick-question.ts create mode 100644 server/tests/quick-question.test.ts create mode 100644 web/src/lib/quick-question.ts create mode 100644 web/tests/quick-question.test.mjs diff --git a/docs/api/streaming-chat.md b/docs/api/streaming-chat.md index 02950bf..ac99b63 100644 --- a/docs/api/streaming-chat.md +++ b/docs/api/streaming-chat.md @@ -1,21 +1,22 @@ # Streaming Chat API Contract -This document defines the server-sent events (SSE) contract for chat completions. +This document defines the server-sent events (SSE) contract for chat completions and Quick Questions. Endpoint: - `POST /v1/chat-completions/stream` - `POST /v1/chats/:chatId/stream/attach` +- `POST /v1/quick-questions/stream` Transport: - HTTP response uses `Content-Type: text/event-stream; charset=utf-8` - Events are emitted in SSE format (`event: ...`, `data: ...`) - Request body is JSON -- Request body supports the same inline attachment schema and limits documented in `docs/api/rest.md`. +- Chat completion request bodies support the same inline attachment schema and limits documented in `docs/api/rest.md`. Quick Questions accept text only. Authentication: - Same as REST endpoints (`Authorization: Bearer ` when token mode is enabled) -## Request Body +## Chat Completion Request Body ```json { @@ -90,6 +91,31 @@ Persisted chat streams with a `chatId` are backend-owned active runs: This endpoint is intended for clients that restored an active `chatId` from `GET /v1/active-runs`, especially after browser refresh. Replayed `delta` events may include text that was originally emitted before the client attached. +## Quick Question Endpoint + +`POST /v1/quick-questions/stream` + +Request body: +```json +{ + "provider": "openai|anthropic|xai|gemini|hermes-agent", + "model": "string", + "question": "What is the capital of France?", + "enabledTools": ["web_search", "fetch_url"], + "userLocation": "optional city, region, country", + "temperature": 0.2, + "maxTokens": 256 +} +``` + +Behavior notes: +- `question` is required, trimmed by the server, and must not be empty. +- The server prepends a Quick Question system prompt that asks for a succinct, direct, self-contained answer without follow-up questions. Clients do not send or maintain this prompt. +- Quick Questions are always non-persistent. The endpoint does not create a chat or store messages, tool-call logs, assistant output, or `LlmCall` metadata. +- The response uses the same `meta`, `tool_call`, `delta`, `done`, and `error` SSE events as chat completion streams. The `meta` event has `chatId: null` and `callId: null`. +- `enabledTools`, `temperature`, and `maxTokens` are optional and behave as they do for chat completion streams. When `enabledTools` is omitted, all available Sybil-managed tools are enabled by default. +- User location is inferred from the same request headers as chat completion streams when `userLocation` is omitted. + ## Event Stream Contract Event order: diff --git a/ios/Packages/Sybil/Sources/Sybil/SybilAPIClient.swift b/ios/Packages/Sybil/Sources/Sybil/SybilAPIClient.swift index d4c7f14..50b3f7d 100644 --- a/ios/Packages/Sybil/Sources/Sybil/SybilAPIClient.swift +++ b/ios/Packages/Sybil/Sources/Sybil/SybilAPIClient.swift @@ -183,6 +183,29 @@ actor SybilAPIClient: SybilAPIClienting { SybilLog.info(SybilLog.network, "Chat stream completed") } + func runQuickQuestionStream( + body: QuickQuestionStreamRequest, + onEvent: @escaping @Sendable (CompletionStreamEvent) async -> Void + ) async throws { + let request = try makeRequest( + path: "/v1/quick-questions/stream", + method: "POST", + body: AnyEncodable(body), + acceptsSSE: true + ) + + SybilLog.info( + SybilLog.network, + "Starting quick question stream POST \(request.url?.absoluteString ?? "")" + ) + + try await stream(request: request) { eventName, dataText in + try await Self.handleCompletionStreamEvent(eventName: eventName, dataText: dataText, onEvent: onEvent) + } + + SybilLog.info(SybilLog.network, "Quick question stream completed") + } + func attachCompletionStream( chatID: String, onEvent: @escaping @Sendable (CompletionStreamEvent) async -> Void @@ -664,6 +687,12 @@ struct CompletionStreamRequest: Codable, Sendable { var userLocation: String? = nil } +struct QuickQuestionStreamRequest: Codable, Sendable { + var provider: Provider + var model: String + var question: String +} + private struct ChatCreateBody: Encodable { var title: String? var provider: Provider? diff --git a/ios/Packages/Sybil/Sources/Sybil/SybilAPIClienting.swift b/ios/Packages/Sybil/Sources/Sybil/SybilAPIClienting.swift index c26ceba..75a81d4 100644 --- a/ios/Packages/Sybil/Sources/Sybil/SybilAPIClienting.swift +++ b/ios/Packages/Sybil/Sources/Sybil/SybilAPIClienting.swift @@ -27,6 +27,10 @@ protocol SybilAPIClienting: Sendable { body: CompletionStreamRequest, onEvent: @escaping @Sendable (CompletionStreamEvent) async -> Void ) async throws + func runQuickQuestionStream( + body: QuickQuestionStreamRequest, + onEvent: @escaping @Sendable (CompletionStreamEvent) async -> Void + ) async throws func attachCompletionStream( chatID: String, onEvent: @escaping @Sendable (CompletionStreamEvent) async -> Void diff --git a/ios/Packages/Sybil/Sources/Sybil/SybilViewModel.swift b/ios/Packages/Sybil/Sources/Sybil/SybilViewModel.swift index 0dbe5d9..672d905 100644 --- a/ios/Packages/Sybil/Sources/Sybil/SybilViewModel.swift +++ b/ios/Packages/Sybil/Sources/Sybil/SybilViewModel.swift @@ -1162,13 +1162,11 @@ final class SybilViewModel { let streamStatus = CompletionStreamStatus() do { - try await client().runCompletionStream( - body: CompletionStreamRequest( - chatId: nil, - persist: false, + try await client().runQuickQuestionStream( + body: QuickQuestionStreamRequest( provider: provider, model: model, - messages: [CompletionRequestMessage(role: .user, content: prompt)] + question: prompt ) ) { [weak self] event in guard let self else { return } diff --git a/ios/Packages/Sybil/Tests/SybilTests/SybilTests.swift b/ios/Packages/Sybil/Tests/SybilTests/SybilTests.swift index 3e223c8..c56bb37 100644 --- a/ios/Packages/Sybil/Tests/SybilTests/SybilTests.swift +++ b/ios/Packages/Sybil/Tests/SybilTests/SybilTests.swift @@ -22,6 +22,7 @@ private struct MockClientCallSnapshot: Sendable { var getSearch = 0 var getActiveRuns = 0 var runCompletionStream = 0 + var runQuickQuestionStream = 0 var attachCompletionStream = 0 var attachSearchStream = 0 } @@ -50,7 +51,7 @@ private actor MockSybilClient: SybilAPIClienting { private var snapshot = MockClientCallSnapshot() private var lastCreateChatCall: ChatCreateCallSnapshot? - private var lastCompletionStreamBody: CompletionStreamRequest? + private var lastQuickQuestionStreamBody: QuickQuestionStreamRequest? private var completionStreamEvents: [CompletionStreamEvent]? private var listChatsDelayNanoseconds: UInt64 = 0 private var listSearchesDelayNanoseconds: UInt64 = 0 @@ -103,8 +104,8 @@ private actor MockSybilClient: SybilAPIClienting { lastCreateChatCall } - func currentCompletionStreamBody() -> CompletionStreamRequest? { - lastCompletionStreamBody + func currentQuickQuestionStreamBody() -> QuickQuestionStreamRequest? { + lastQuickQuestionStreamBody } func setCompletionStreamEvents(_ events: [CompletionStreamEvent], delayNanoseconds: UInt64 = 0) { @@ -287,7 +288,27 @@ private actor MockSybilClient: SybilAPIClienting { onEvent: @escaping @Sendable (CompletionStreamEvent) async -> Void ) async throws { snapshot.runCompletionStream += 1 - lastCompletionStreamBody = body + if completionStreamDelayNanoseconds > 0 { + try await Task.sleep(nanoseconds: completionStreamDelayNanoseconds) + } + if let completionStreamNetworkErrorMessage { + throw APIError.networkError(message: completionStreamNetworkErrorMessage) + } + if let completionStreamEvents { + for event in completionStreamEvents { + await onEvent(event) + } + return + } + throw UnexpectedClientCall() + } + + func runQuickQuestionStream( + body: QuickQuestionStreamRequest, + onEvent: @escaping @Sendable (CompletionStreamEvent) async -> Void + ) async throws { + snapshot.runQuickQuestionStream += 1 + lastQuickQuestionStreamBody = body if completionStreamDelayNanoseconds > 0 { try await Task.sleep(nanoseconds: completionStreamDelayNanoseconds) } @@ -1005,7 +1026,8 @@ private func makeToolCallMessage(id: String, date: Date, summary: String = "Ran await first?.value let calls = await client.currentSnapshot() - #expect(calls.runCompletionStream == 1) + #expect(calls.runQuickQuestionStream == 1) + #expect(calls.runCompletionStream == 0) #expect(viewModel.quickQuestionAnswerText == "One answer.") #expect(!viewModel.isQuickQuestionSending) } @@ -1021,12 +1043,12 @@ private func makeToolCallMessage(id: String, date: Date, summary: String = "Ran await task?.value let calls = await client.currentSnapshot() - #expect(calls.runCompletionStream == 0) + #expect(calls.runQuickQuestionStream == 0) #expect(!viewModel.isQuickQuestionSending) } @MainActor -@Test func quickQuestionRunsNonPersistentCompletionStream() async throws { +@Test func quickQuestionUsesDedicatedServerEndpoint() async throws { let client = MockSybilClient() await client.setCompletionStreamEvents([ .delta(CompletionStreamDelta(text: "Reset it from ")), @@ -1041,13 +1063,11 @@ private func makeToolCallMessage(id: String, date: Date, summary: String = "Ran await task?.value let snapshot = await client.currentSnapshot() - let body = await client.currentCompletionStreamBody() - #expect(snapshot.runCompletionStream == 1) - #expect(body?.persist == false) - #expect(body?.chatId == nil) + let body = await client.currentQuickQuestionStreamBody() + #expect(snapshot.runQuickQuestionStream == 1) + #expect(snapshot.runCompletionStream == 0) #expect(body?.provider == .openai) - #expect(body?.messages.first?.role == .user) - #expect(body?.messages.first?.content == "How do I reset my password?") + #expect(body?.question == "How do I reset my password?") #expect(viewModel.quickQuestionAnswerText == "Reset it from Settings.") #expect(!viewModel.isQuickQuestionSending) } @@ -1572,7 +1592,7 @@ private final class MockQuickQuestionLifecycle { #expect(panel.state.prompt == "Keep this draft") #expect(panel.state.answer == "An answer arrived while hidden.") let calls = await client.currentSnapshot() - #expect(calls.runCompletionStream == 0) + #expect(calls.runQuickQuestionStream == 0) controller.stop() } @@ -1630,7 +1650,7 @@ private final class MockQuickQuestionLifecycle { #expect(panel.presentationCount == 2) #expect(panel.state.canSend) let calls = await client.currentSnapshot() - #expect(calls.runCompletionStream == 0) + #expect(calls.runQuickQuestionStream == 0) controller.stop() } @@ -1652,7 +1672,7 @@ private final class MockQuickQuestionLifecycle { #expect(panel.state.prompt == "A signed-out question") #expect(!viewModel.isQuickQuestionSending) let calls = await client.currentSnapshot() - #expect(calls.runCompletionStream == 0) + #expect(calls.runQuickQuestionStream == 0) controller.stop() } #endif diff --git a/server/src/llm/quick-question.ts b/server/src/llm/quick-question.ts new file mode 100644 index 0000000..4431a42 --- /dev/null +++ b/server/src/llm/quick-question.ts @@ -0,0 +1,30 @@ +import type { MultiplexRequest, Provider } from "./types.js"; + +export const QUICK_QUESTION_SYSTEM_PROMPT = + "You are answering a quick question in a one-shot experience intended to replace a quick Google search. " + + "Give a succinct, direct, self-contained answer. Do not ask follow-up questions, invite the user to continue, " + + "or offer additional help. If the question is ambiguous, make the most reasonable assumption and state it briefly only when needed."; + +export type QuickQuestionRequest = { + provider: Provider; + model: string; + question: string; + enabledTools?: string[]; + userLocation?: string; + temperature?: number; + maxTokens?: number; +}; + +export function buildQuickQuestionMultiplexRequest({ + question, + ...request +}: QuickQuestionRequest): MultiplexRequest { + return { + ...request, + persist: false, + messages: [ + { role: "system", content: QUICK_QUESTION_SYSTEM_PROMPT }, + { role: "user", content: question }, + ], + }; +} diff --git a/server/src/routes.ts b/server/src/routes.ts index 9f7379c..538d8fa 100644 --- a/server/src/routes.ts +++ b/server/src/routes.ts @@ -11,6 +11,7 @@ import { runMultiplex } from "./llm/multiplexer.js"; import { runMultiplexStream, type StreamEvent } from "./llm/streaming.js"; import { getAvailableChatTools, normalizeEnabledChatTools } from "./llm/chat-tools.js"; import { getModelCatalogSnapshot } from "./llm/model-catalog.js"; +import { buildQuickQuestionMultiplexRequest } from "./llm/quick-question.js"; import { openaiClient } from "./llm/providers.js"; import { serializeProviderFields, toPrismaProvider } from "./llm/provider-ids.js"; import { exaClient } from "./search/exa.js"; @@ -205,6 +206,16 @@ const CompletionStreamBody = z } }); +const QuickQuestionStreamBody = z.object({ + provider: ProviderSchema, + model: z.string().min(1), + question: z.string().trim().min(1), + enabledTools: EnabledToolsSchema.optional(), + userLocation: z.string().trim().min(1).max(200).optional(), + temperature: z.number().min(0).max(2).optional(), + maxTokens: z.number().int().positive().optional(), +}); + function mergeAttachmentsIntoMetadata(metadata: unknown, attachments?: ChatAttachment[]) { if (!attachments?.length) return metadata as any; if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) { @@ -1564,7 +1575,28 @@ export async function registerRoutes(app: FastifyInstance) { }; }); - // Streaming SSE endpoint. + // One-shot, non-persistent Quick Question SSE endpoint. + app.post("/v1/quick-questions/stream", async (req, reply) => { + requireAdmin(req); + + const parsed = QuickQuestionStreamBody.safeParse(req.body); + if (!parsed.success) return app.httpErrors.badRequest(parsed.error.message); + const body = withRequestUserLocation(parsed.data, req); + + reply.raw.writeHead(200, buildSseHeaders(typeof req.headers.origin === "string" ? req.headers.origin : undefined)); + reply.raw.flushHeaders(); + + for await (const ev of runMultiplexStream(buildQuickQuestionMultiplexRequest(body))) { + writeSseEvent(reply, mapChatStreamEvent(ev)); + } + + if (!reply.raw.destroyed && !reply.raw.writableEnded) { + reply.raw.end(); + } + return reply; + }); + + // General chat completion SSE endpoint. app.post("/v1/chat-completions/stream", async (req, reply) => { requireAdmin(req); diff --git a/server/tests/quick-question.test.ts b/server/tests/quick-question.test.ts new file mode 100644 index 0000000..ca1b77f --- /dev/null +++ b/server/tests/quick-question.test.ts @@ -0,0 +1,27 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { + buildQuickQuestionMultiplexRequest, + QUICK_QUESTION_SYSTEM_PROMPT, +} from "../src/llm/quick-question.js"; + +test("quick question requests receive the server-owned one-shot system prompt", () => { + const request = buildQuickQuestionMultiplexRequest({ + provider: "openai", + model: "gpt-4.1-mini", + question: "How do I reset my password?", + enabledTools: ["web_search"], + userLocation: "Los Angeles, CA", + }); + + assert.equal(request.persist, false); + assert.equal(request.chatId, undefined); + assert.deepEqual(request.messages, [ + { role: "system", content: QUICK_QUESTION_SYSTEM_PROMPT }, + { role: "user", content: "How do I reset my password?" }, + ]); + assert.deepEqual(request.enabledTools, ["web_search"]); + assert.equal(request.userLocation, "Los Angeles, CA"); + assert.match(QUICK_QUESTION_SYSTEM_PROMPT, /succinct, direct, self-contained answer/i); + assert.match(QUICK_QUESTION_SYSTEM_PROMPT, /do not ask follow-up questions/i); +}); diff --git a/web/src/App.tsx b/web/src/App.tsx index 17a55e4..141cc79 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -43,6 +43,7 @@ import { getSearch, listWorkspaceItems, runCompletionStream, + runQuickQuestionStream, runSearchStream, suggestChatTitle, updateChatTitle, @@ -79,6 +80,7 @@ import { resolveSidebarSelectionAfterRefresh, type SidebarSelection, } from "@/lib/sidebar-selection"; +import { buildQuickQuestionRequest } from "@/lib/quick-question"; import { cn } from "@/lib/utils"; type DraftSelectionKind = "chat" | "search"; @@ -3225,13 +3227,12 @@ export default function App() { let streamErrorMessage: string | null = null; try { - await runCompletionStream( - { - persist: false, + await runQuickQuestionStream( + buildQuickQuestionRequest({ provider: quickProvider, model: selectedModel, - messages: [{ role: "user", content }], - }, + content, + }), { onToolCall: (payload) => { setQuickQuestionMessages((current) => { diff --git a/web/src/lib/api.ts b/web/src/lib/api.ts index 977514e..95cddd4 100644 --- a/web/src/lib/api.ts +++ b/web/src/lib/api.ts @@ -191,6 +191,14 @@ type CompletionStreamHandlers = { onError?: (payload: { message: string }) => void; }; +function dispatchCompletionStreamEvent(handlers: CompletionStreamHandlers, eventName: string, payload: any) { + if (eventName === "meta") handlers.onMeta?.(payload); + else if (eventName === "tool_call") handlers.onToolCall?.(payload); + else if (eventName === "delta") handlers.onDelta?.(payload); + else if (eventName === "done") handlers.onDone?.(payload); + else if (eventName === "error") handlers.onError?.(payload); +} + type CreateChatRequest = { title?: string; provider?: Provider; @@ -627,13 +635,34 @@ export async function runCompletionStream( signal: options?.signal, }); - await readSseStream(response, (eventName, payload) => { - if (eventName === "meta") handlers.onMeta?.(payload); - else if (eventName === "tool_call") handlers.onToolCall?.(payload); - else if (eventName === "delta") handlers.onDelta?.(payload); - else if (eventName === "done") handlers.onDone?.(payload); - else if (eventName === "error") handlers.onError?.(payload); + await readSseStream(response, (eventName, payload) => dispatchCompletionStreamEvent(handlers, eventName, payload)); +} + +export async function runQuickQuestionStream( + body: { + provider: Provider; + model: string; + question: string; + }, + handlers: CompletionStreamHandlers, + options?: { signal?: AbortSignal } +) { + const headers = new Headers({ + Accept: "text/event-stream", + "Content-Type": "application/json", }); + if (authToken) { + headers.set("Authorization", `Bearer ${authToken}`); + } + + const response = await fetch(`${API_BASE_URL}/v1/quick-questions/stream`, { + method: "POST", + headers, + body: JSON.stringify(body), + signal: options?.signal, + }); + + await readSseStream(response, (eventName, payload) => dispatchCompletionStreamEvent(handlers, eventName, payload)); } export async function attachCompletionStream(chatId: string, handlers: CompletionStreamHandlers, options?: { signal?: AbortSignal }) { @@ -650,11 +679,5 @@ export async function attachCompletionStream(chatId: string, handlers: Completio signal: options?.signal, }); - await readSseStream(response, (eventName, payload) => { - if (eventName === "meta") handlers.onMeta?.(payload); - else if (eventName === "tool_call") handlers.onToolCall?.(payload); - else if (eventName === "delta") handlers.onDelta?.(payload); - else if (eventName === "done") handlers.onDone?.(payload); - else if (eventName === "error") handlers.onError?.(payload); - }); + await readSseStream(response, (eventName, payload) => dispatchCompletionStreamEvent(handlers, eventName, payload)); } diff --git a/web/src/lib/quick-question.ts b/web/src/lib/quick-question.ts new file mode 100644 index 0000000..40944ca --- /dev/null +++ b/web/src/lib/quick-question.ts @@ -0,0 +1,17 @@ +import type { Provider } from "./api"; + +export function buildQuickQuestionRequest({ + provider, + model, + content, +}: { + provider: Provider; + model: string; + content: string; +}) { + return { + provider, + model, + question: content, + }; +} diff --git a/web/tests/quick-question.test.mjs b/web/tests/quick-question.test.mjs new file mode 100644 index 0000000..4b1beab --- /dev/null +++ b/web/tests/quick-question.test.mjs @@ -0,0 +1,18 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { buildQuickQuestionRequest } from "../src/lib/quick-question.ts"; + +test("quick question requests use the dedicated server endpoint shape", () => { + const request = buildQuickQuestionRequest({ + provider: "openai", + model: "gpt-4.1-mini", + content: "How do I reset my password?", + }); + + assert.deepEqual(request, { + provider: "openai", + model: "gpt-4.1-mini", + question: "How do I reset my password?", + }); + assert.equal("additionalSystemPrompt" in request, false); +}); diff --git a/web/tsconfig.tsbuildinfo b/web/tsconfig.tsbuildinfo index 16587b6..304d97a 100644 --- a/web/tsconfig.tsbuildinfo +++ b/web/tsconfig.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/App.tsx","./src/main.tsx","./src/pwa.ts","./src/root-router.tsx","./src/vite-env.d.ts","./src/components/sybil-character.tsx","./src/components/auth/auth-screen.tsx","./src/components/chat/chat-attachment-list.tsx","./src/components/chat/chat-composer.tsx","./src/components/chat/chat-messages-panel.tsx","./src/components/markdown/markdown-content.tsx","./src/components/search/search-results-panel.tsx","./src/components/ui/button.tsx","./src/components/ui/input.tsx","./src/components/ui/scroll-area.tsx","./src/components/ui/separator.tsx","./src/components/ui/textarea.tsx","./src/hooks/use-session-auth.ts","./src/lib/api.ts","./src/lib/chat-forking.ts","./src/lib/chat-model-selection.ts","./src/lib/sidebar-selection.ts","./src/lib/utils.ts","./src/pages/search-route-page.tsx"],"version":"5.9.3"} \ No newline at end of file +{"root":["./src/App.tsx","./src/main.tsx","./src/pwa.ts","./src/root-router.tsx","./src/vite-env.d.ts","./src/components/sybil-character.tsx","./src/components/auth/auth-screen.tsx","./src/components/chat/chat-attachment-list.tsx","./src/components/chat/chat-composer.tsx","./src/components/chat/chat-messages-panel.tsx","./src/components/markdown/markdown-content.tsx","./src/components/search/search-results-panel.tsx","./src/components/ui/button.tsx","./src/components/ui/input.tsx","./src/components/ui/scroll-area.tsx","./src/components/ui/separator.tsx","./src/components/ui/textarea.tsx","./src/hooks/use-session-auth.ts","./src/lib/api.ts","./src/lib/chat-forking.ts","./src/lib/chat-model-selection.ts","./src/lib/quick-question.ts","./src/lib/sidebar-selection.ts","./src/lib/utils.ts","./src/pages/search-route-page.tsx"],"version":"5.9.3"} \ No newline at end of file