From f80c8644dacbf262cda18d84629b459cc3a88011 Mon Sep 17 00:00:00 2001 From: Paul Miller Date: Tue, 26 Aug 2025 15:39:19 -0500 Subject: [PATCH 1/5] max height and overflow scroll for ModelSelector --- frontend/src/components/ModelSelector.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/ModelSelector.tsx b/frontend/src/components/ModelSelector.tsx index 4d3843e6a..2aad44a3a 100644 --- a/frontend/src/components/ModelSelector.tsx +++ b/frontend/src/components/ModelSelector.tsx @@ -280,7 +280,7 @@ export function ModelSelector({ - + {availableModels && Array.isArray(availableModels) && // Sort models: vision-capable first (if images present), then available, then restricted, then disabled From 65705cc53f4fe5e583b0c9e6645a0563669c5945 Mon Sep 17 00:00:00 2001 From: Paul Miller Date: Tue, 26 Aug 2025 17:15:19 -0500 Subject: [PATCH 2/5] wip working responses chat creation --- NEW_UPDATE_TO_CONVERSATIONS_PLAN.md | 223 ++++++++++++++++++++++++ UPDATE_TO_RESPONSES_API.md | 156 +++++++++++++++++ frontend/bun.lock | 246 ++++++++++++++++----------- frontend/package.json | 4 +- frontend/src/ai/OpenAIContext.tsx | 37 ++-- frontend/src/hooks/useChatSession.ts | 177 ++++++++++++------- 6 files changed, 663 insertions(+), 180 deletions(-) create mode 100644 NEW_UPDATE_TO_CONVERSATIONS_PLAN.md create mode 100644 UPDATE_TO_RESPONSES_API.md diff --git a/NEW_UPDATE_TO_CONVERSATIONS_PLAN.md b/NEW_UPDATE_TO_CONVERSATIONS_PLAN.md new file mode 100644 index 000000000..f3235ce13 --- /dev/null +++ b/NEW_UPDATE_TO_CONVERSATIONS_PLAN.md @@ -0,0 +1,223 @@ +# Maple: Migration Plan to OpenAI Conversations + Responses + +This plan completes Maple’s end-to-end migration from the legacy “threads” model to OpenAI-compatible Conversations + Responses APIs. It aligns Maple with the implementation in `../opensecret/src/web/conversations.rs` and patterns from `../openai-responses-poc`. + +Outcome goals: +- Legacy chats still load (KV storage), but are read-only (no new messages). +- All new chats are first-class Conversations; message sends use Responses API scoped to a Conversation. +- Frontend no longer owns chat history persistence; state is server-managed and retrieved via Conversations endpoints. +- Robust streaming with correct event handling; unknown/unsupported events are logged for diagnosis. + +Decisions confirmed: +- URL redirects: Provide a minimal, centralized redirect for old URLs. If we cannot map a historical Response ID to a Conversation ID via API, redirect to home with a notice. Keep logic in one place with a TODO to enhance backend (`GET /responses/{id}` include conversation_id) for perfect mapping. +- Titles: Backend populates metadata; show placeholder “New Conversation” while awaiting server title. +- Rename/Delete: Implement fully for Conversation-backed chats now (`conversations.update/delete`). +- Legacy migration: Show a simple read-only banner; migration UX is future work. +- History pagination: Future enhancement; for now, load all items. +- Web search/tools: Add TODO + console logging hooks referencing POC locations for follow-up. +- Images: Future enhancement; first pass is text-only. + +References: +- Backend Conversations: `../opensecret/src/web/conversations.rs` +- Backend Responses: `../opensecret/src/web/responses.rs` (expects `conversation`, deprecates `previous_response_id`) +- POC patterns: `../openai-responses-poc/frontend/src/contexts/ConversationContext.tsx`, `../openai-responses-poc/frontend/src/lib/openai-client.ts`, `../openai-responses-poc/frontend/src/lib/streaming.ts` +- OpenSecret SDK: `../OpenSecret-SDK/src/lib/api.ts` (`listConversations`, conversations items helpers) + +— + +## Architecture Changes + +- Chat identity: `chatId` becomes the Conversation ID (UUID). We do NOT use Response IDs for routing. +- Continuation: pass `conversation: ` to `openai.responses.create(...)` rather than `previous_response_id`. +- History retrieval: load messages via `GET /v1/conversations/:id/items` (OpenAI-compatible) instead of storing client-side. +- Conversation list: sidebar populated from the custom `GET /v1/conversations` endpoint exposed via the OpenSecret SDK `listConversations()`. +- Legacy chats: detected from KV; render-only. UI prevents sending and shows a read-only banner. + +— + +## Step-by-Step Plan + +1) Wire Conversation List (Sidebar) +- Replace usage of `fetchResponsesList` with `listConversations({ limit, after, before })` in `frontend/src/state/LocalStateContext.tsx`. +- Map item title from `conversation.metadata?.title ?? "New Conversation"` (backend stores title in metadata). +- Merge legacy history (KV) with Conversations list for a unified sidebar view, de-duped by `id`. +- Keep sort by `updated_at`/`created_at` as today; Conversations provide `created_at`. If `updated_at` is needed, approximate with last item timestamp (follow-up). + +2) Load Conversation Items for Chat View +- Update `getChatById(id)` in `LocalStateContext.tsx`: + - If KV entry exists → legacy chat (read-only), return as-is. + - Else, treat `id` as Conversation ID: fetch items via `openai.conversations.items.list(id, { order: 'asc' })`. + - Map items to Maple `ChatMessage[]`: + - `type === 'message'` with `role in {user,assistant}` → extract text from content array. + - Ignore or log other item types for now (e.g., tool calls); preserve for future UI. + - Return `{ id, title, model?, messages }` without persisting to KV. +- Remove localStorage fallback `responses_chat_*` writes and reads. + +3) Create + Continue Chats Using Conversations +- Update `useChatSession` to adopt Conversation-first flow: + - NEW chat: + - Call `openai.conversations.create({})` immediately. + - Navigate/replace URL to `/chat/` when created. + - Then call `openai.responses.create({ model, conversation: , input: , stream: true, store: true })`. + - EXISTING chat: + - Use `conversation: chatId` on all subsequent `responses.create` calls. + - Remove `previous_response_id` entirely (backend ignores it and prefers `conversation`). +- Input shape: keep simple `input: string` for now (backend supports; images later). + +4) Streaming Event Handling (Responses API) +- Consume SSE frames compatible with backend events (see `responses.rs`): + - `response.created` → no-op except ensure local state shows “streaming”. + - `response.in_progress` → optional UI signal. + - `response.output_item.added` → begin assistant message. + - `response.content_part.added` → can be ignored for text-only, or prepare part indices. + - `response.output_text.delta` → append to streaming buffer. + - `response.output_text.done` → optional; finalization occurs on completed. + - `response.completed` → finalize assistant message, invalidate conversation list. + - `response.error` → present banner and end stream. +- For any unknown `event.type`, log to console with the raw frame. This unblocks future tool/web-search types. +- Abort handling: keep `AbortController` semantics; don’t surface abort as an error. + +5) Make Legacy Chats Read-Only +- Detect legacy via `getChatById` (KV hit) and render a banner: “This is a legacy chat. You can read it but not reply.” +- Disable input submit for legacy chats (prop from route → `ChatBox`). +- Prevent `appendUserMessage` from sending on legacy chats (guard in `useChatSession`). + +6) Titles & Rename/Delete Behavior +- Titles: default to metadata.title from `listConversations()`. If absent, show “New Conversation”. +- Option A (minimal): hide “Rename” for Conversation-backed chats until we wire `openai.conversations.update(id, { metadata: { title }})`. +- Option B (preferred): wire rename for Conversations now; keep KV rename for legacy. +- Delete behavior: + - Conversations: call `openai.conversations.delete(id)` then invalidate list. + - Legacy: current KV delete path. + +7) Cleanup Frontend State +- Remove Responses-list history fetches and any writes to `localStorage` for Responses. +- Delete the “handoff” context state where possible (userPrompt/systemPrompt/image handoff), favoring in-place start on home and URL `replaceState` (future refactor matches POC). +- Keep `draftMessages` as-is if still used purely for input UX. + +8) Error Handling & Observability +- Replace generic “Failed to create response stream” with actionable messages: + - Auth missing (OpenSecret context unavailable) + - Network/HTTP error with status + server error message + - Unsupported event types (logged) +- Add `console.debug` around request payloads (without user content) to verify `conversation` is always sent. + +9) Testing & Validation +- Manual scenarios: + - New chat: create conversation → stream model deltas → title appears in sidebar. + - Continue existing conversation (refresh and send again): no prior messages passed in request; conversation context is honored. + - Legacy chat opens; input disabled; cannot send. + - Abort mid-stream; no banner; can send again. + - Error case: surface `response.error` frame. +- Optional: record one SSE stream and verify parser accumulates text correctly. + +— + +## File-Level Changes (Minimal, Surgical) + +- `frontend/src/state/LocalStateContext.tsx` + - Swap `fetchResponsesList` → `listConversations` for history. + - In `getChatById`, add Conversations branch: fetch items via `openai.conversations.items.list` and map to `ChatMessage[]`. + - Remove localStorage `responses_chat_*` reads/writes. + - Gate `renameChat`/`deleteChat` by storage mode (KV vs Conversations). Optionally add new helpers using OpenAI client for Conversations. + +- `frontend/src/hooks/useChatSession.ts` + - Introduce Conversation lifecycle: + - On new chat: create conversation, set URL, then send response with `conversation`. + - On existing: always send with `conversation: chatId`. + - Remove `previous_response_id` usage. + - Expand event handling; log unknown events. + - Guard append on legacy (read-only) chats. + - Centralize redirect handling for old URLs: detect non-conversation UUIDs; if not a KV chat and not a valid conversation, attempt `fetchResponse(id)`. If mapping to conversation is not possible (current backend), redirect to home and show a “Chat moved” notice. Add TODO to switch to perfect redirect when backend returns `conversation_id` on response retrieval. + +- `frontend/src/routes/_auth.chat.$chatId.tsx` + - Use new `useChatSession` semantics; disable input when chat is legacy. + - Remove reliance on response IDs to mutate URL; now URL is set when the conversation is created. + - Add minimal redirect surface (single place) that handles the old URL flow described above. + +- `frontend/src/components/ChatHistoryList.tsx` + - Optionally hide Rename for Conversations until wired; otherwise call Conversations update. + +- Delete/clean any code that references “Responses list” as a source of truth for chat history. + +— + +## Implementation Checklist + +API-correctness first +- [ ] Replace `previous_response_id` with `conversation` param in all sends +- [ ] Create conversation before first send; update URL via `replaceState` +- [ ] Ensure Responses stream consumption handles event types per backend + +Conversation list and loading +- [ ] Sidebar uses `listConversations()` from SDK (no responses list) +- [ ] Titles show placeholder “New Conversation” until backend metadata present +- [ ] `getChatById` loads items via `conversations.items.list(order: 'asc')` +- [ ] Map items to messages; log unsupported types (web search/tool) with TODO + POC references + +Legacy handling +- [ ] Legacy KV chats open; input disabled; banner shown +- [ ] All new chats are Conversations (no KV writes for them) + +Rename/Delete +- [ ] Implement `conversations.update(id, { metadata: { title } })` +- [ ] Implement `conversations.delete(id)` and invalidate list +- [ ] Keep KV rename/delete for legacy + +Redirects +- [ ] Centralized redirect: detect old URLs; try conversation retrieve; fallback to response retrieve +- [ ] If cannot map Response → Conversation, redirect to home with notice +- [ ] TODO: remove workaround after backend adds `conversation_id` to `/responses/{id}` + +Cleanup +- [ ] Remove localStorage `responses_chat_*` code paths +- [ ] Remove responses-list-based history paths +- [ ] Delete obsolete thread/previous_response_id logic + +Testing +- [ ] New chat happy path (stream → complete → title appears) +- [ ] Continue existing conversation (no message history in request) +- [ ] Legacy chat read-only UX +- [ ] Abort mid-stream, no error +- [ ] Error frame surfaces to UI + +— + +— + +## Acceptance Criteria + +- New chats always create a Conversation first; route updates to `/chat/` before streaming. +- Sends use `openai.responses.create({ conversation: , input: , stream: true, store: true })`. +- Existing conversation sends do NOT include the full message history nor `previous_response_id`. +- Sidebar lists Conversations via `listConversations()` and legacy KV chats; titles come from conversation metadata or fallback. +- Legacy chats open but are read-only. +- Streaming works without “Failed to create response stream”; unknown event types are logged. + +Note: We prioritize making the API usage correct first (conversation-scoped sends and creation order) to eliminate the current stream error, then complete sidebar/history and cleanup. + +— + +## Open Questions / Decisions Needed + +1) URL semantics: Can we definitively change `chatId` to mean Conversation ID only? Should we add a compatibility redirect if a user opens a historical URL that contains a Response ID? +2) Titles: Do we want client-side title generation (as in POC) for brand-new conversations, or will the backend populate metadata.title (synchronously or via background job)? +3) Rename/Delete: Should we wire Conversations `update/delete` now, or temporarily hide rename/delete for Conversation-backed chats? +4) Legacy banner copy: Ok to show a non-intrusive banner (“Legacy chat. Read-only.”) and disable input? Any desire to expose a “Migrate this chat” action? +5) Pagination for conversation items: Is loading full history acceptable for now, or do we need paging/infinite scroll in the first pass? +6) Tool/web-search support: Should we log-only unknown items, or also render lightweight stubs (e.g., a small badge row) similar to the POC’s web search handling? +7) Vision/images: Confirm deferring for now. When we add it, we’ll switch `input` to content parts and support image inputs + conversation items mapping. + +— + +## Rollout + +- Feature-flag: gate Conversations path for a small cohort; default on after sanity checks. +- Dogfood in development/staging; verify cross-device behavior (Conversations persist server-side). +- Remove legacy Responses-list dependencies after stable. + +— + +## Notes on Current Failure + +- The “Failed to create response stream” likely comes from sending `previous_response_id` without a `conversation`. Backend `responses.rs` treats `previous_response_id` as deprecated and does not use it; without `conversation`, a new conversation is created for each send, and client treats Response IDs as thread identifiers. Switching to explicit `conversation` parameter aligns with backend and fixes continuity. diff --git a/UPDATE_TO_RESPONSES_API.md b/UPDATE_TO_RESPONSES_API.md new file mode 100644 index 000000000..7c8cca6a4 --- /dev/null +++ b/UPDATE_TO_RESPONSES_API.md @@ -0,0 +1,156 @@ +## Update Plan: Migrate Frontend to Responses API + +### Goals +- Step 1: Send new chats using the Responses API (not Chat Completions) +- Step 2: Stream/subscribe to the correct response via SSE and render deltas +- Step 3: Add new chats to the sidebar using the response ID and server‑generated title +- Future Work: Loader/migration for old chats into Responses +- Future Work: Unify chat UX (start on home, push `chatId` into URL, no full page swap) + +### References +- Backend implementation notes: `opensecret/docs/responses-implementation.md` +- OpenAPI surface: `opensecret/docs/openapi.yml` +- OpenAI migration guide: `opensecret/docs/openai-migrate-doc.md` + +### Decisions (from you) +- System prompts: keep as‑is. We will map the existing `systemPrompt` to the Responses request without changing the UI surface (use `instructions` when calling Responses, logic stays the same). +- Titles: rely on the Responses list endpoint to return server‑generated titles; fallback to "New Chat" if undefined. +- Vision/images: defer for first pass (text‑only). +- Base URL/auth: same base and middleware; continue using `aiCustomFetch`. +- Idempotency: defer; add a TODO for sending `Idempotency-Key` in a later pass. + +--- + +## High‑Level Approach +1. Leverage the OpenAI client (already configured with `aiCustomFetch`) and the OpenSecret SDK: + - Use `openai.responses.create({ model, input, instructions?, stream: true })` to start a new chat + - Consume the returned async iterator of events (SDK emits OpenAI‑compatible events) +2. Update chat creation flow to detect “new chat” and route through Responses API: + - Build request payload from current UI state (model, input, optional system prompt, metadata) + - For brand‑new threads, omit `previous_response_id` + - Receive the response ID from `response.created` and treat it as the `chatId` +3. UI streaming: + - Replace `openai.beta.chat.completions.stream` with `openai.responses.create(..., stream:true)` + - Accumulate deltas into `currentStreamingMessage` + - Handle abort, transient errors, and server error events +4. Sidebar & titles: + - On `response.completed`, invalidate the history query and fetch via the SDK `fetchResponsesList` which includes titles + - Use server‑generated titles; remove client title generation for Responses‑backed chats +5. Backward compatibility: + - Maintain legacy path for old chats (flag per chat: `storageMode: 'responses' | 'legacy'`) + - Keep existing persistence for legacy chats until migration + +--- + +## Detailed Plan by Step + +### Step 1 — Send new chats to Responses API +- Detection: A “new chat” means no prior messages stored for the current `chatId`. +- Build request (aligned to Responses spec in backend doc): + - `model`: selected model + - `input`: user text (text‑only for first pass) + - `instructions`: pass current `systemPrompt` when set (keeps system prompts as‑is from UI) + - `stream`: true + - `store`: true (server persists) + - `previous_response_id`: null (new thread) + - `metadata`: optional; omit for now +- Call via SDK: `const stream = await openai.responses.create({...})` +- Begin consuming events immediately (see Step 2). + +Code changes (initial): +- `frontend/src/hooks/useChatSession.ts` + - Branch in `appendUserMessage` for new chat: call `openai.responses.create` + - Remove local title generation for Responses mode + - Use `AbortController` to cancel the stream + +### Step 2 — View the incoming SSE stream +- Parse and handle events based on backend spec (see `responses-implementation.md`): + - `response.created`: capture `response.id` and set it as current `chatId` if needed (navigate early if appropriate) + - `response.in_progress`: optional UI signal + - `response.output_item.added` / `response.content_part.added`: initialize structures for deltas + - `response.output_text.delta`: append to `currentStreamingMessage` + - `response.output_text.done` / `response.output_item.done`: finalize message block + - `response.completed`: close stream, trigger invalidations (history, specific chat) +- Error frames: surface to UI and end stream properly; keep DB write robustness server‑side. +- Abort: if user navigates away or starts another send, abort controller cancels the stream. + +Code changes (initial): +- `frontend/src/hooks/useChatSession.ts` + - Replace the streaming loop to drive off events from `openai.responses.create` iterator +- Minor adjustments in `frontend/src/routes/_auth.chat.$chatId.tsx` to treat `currentStreamingMessage` as the single assistant delta buffer + +### Step 3 — Add new chats to the sidebar with correct ID and server title +- Use the response UUID as the `chatId` (per backend: first message UUID = thread UUID for new threads) +- Immediately navigate using that `chatId` once `response.created` arrives (optional; or navigate after completion) +- On `response.completed`, invalidate: + - `['chatHistory']` so the sidebar refreshes and shows the decrypted, server‑generated title + - `['chat', chatId]` if we add a chat details query endpoint +- UI should not generate titles client‑side for Responses chats; show a placeholder (e.g., “Generating title…”) until the list refresh returns the title + +Code changes (initial): +- `frontend/src/routes/_auth.chat.$chatId.tsx` + - When starting a new chat, do not call local `addChat(title)`; rely on server persistence + - Ensure navigation uses the server `chatId` (from `response.created`) +- `frontend/src/components/Sidebar` (if needed) + - Ensure `chatHistory` loader uses SDK `fetchResponsesList` for Responses mode + +--- + +## Data & State Model +- New per‑chat flag: `storageMode` + - `legacy`: existing KV‑style + - `responses`: server‑managed threads/messages +- Local optimistic state still used to render the user message immediately and the streaming assistant message. +- Persistence for Responses chats is entirely server‑side; frontend only invalidates queries after completion. + +--- + +## Networking & Auth +- Use existing authenticated fetch/session mechanism (same as other backend requests) +- SSE with `fetch` + `ReadableStream` (EventSource cannot POST) +- Timeouts/heartbeats: backend emits `: heartbeat` frames; keep connection alive + +--- + +## Error Handling +- Map SSE errors to user‑visible banner (reuse existing `streamingError` pattern) +- On abort, suppress error banners +- Keep UI resilient if storage fails on server (server continues streaming per docs) + +--- + +## Testing Plan +- Unit test SSE parser on recorded streams +- Manual test flows: + - New chat happy path (delta stream → completed → title shows in sidebar) + - Abort mid‑stream + - Network hiccup during stream + - Auth error +- Regression: legacy chats still work unchanged + +--- + +## Rollout Plan +- Feature‑flag Responses path for new chats +- Dogfood with a subset of users +- Flip default to Responses for all new chats + +--- + +## Future Work +1) Load and migrate legacy chats to Responses + - Provide a migration UI: select legacy thread → create new Responses thread → seed context + - Mark legacy items as migrated; keep references for back‑links +2) Unify chat interface (single‑page UX) + - Start on home page; push `chatId` into URL + - Avoid full page remount across chat navigation + - Smoothly switch between threads without losing stream state + +--- + +## TODOs (non‑blocking) +- Add `Idempotency-Key` header generation for new chats (configurable expiry) +- Extend to vision/images when ready (map attachments to supported inputs) +- Reconcile any minor type differences for `aiCustomFetch` vs OpenAI client `fetch` signature if needed (adapter wrapper) + + diff --git a/frontend/bun.lock b/frontend/bun.lock index 3ab602753..d019060bf 100644 --- a/frontend/bun.lock +++ b/frontend/bun.lock @@ -4,7 +4,7 @@ "": { "name": "maple", "dependencies": { - "@opensecret/react": "1.4.0", + "@opensecret/react": "file:../../OpenSecret-SDK", "@radix-ui/react-alert-dialog": "^1.1.1", "@radix-ui/react-avatar": "^1.1.0", "@radix-ui/react-dialog": "^1.1.1", @@ -26,7 +26,7 @@ "clsx": "^2.1.1", "gpt-tokenizer": "^3.0.1", "lucide-react": "^0.436.0", - "openai": "^4.56.1", + "openai": "5.16.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-markdown": "^9.0.1", @@ -195,6 +195,10 @@ "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.1", "", {}, "sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA=="], + "@isaacs/balanced-match": ["@isaacs/balanced-match@4.0.1", "", {}, "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ=="], + + "@isaacs/brace-expansion": ["@isaacs/brace-expansion@5.0.0", "", { "dependencies": { "@isaacs/balanced-match": "^4.0.1" } }, "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA=="], + "@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="], "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.8", "", { "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA=="], @@ -207,6 +211,14 @@ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="], + "@microsoft/api-extractor": ["@microsoft/api-extractor@7.52.11", "", { "dependencies": { "@microsoft/api-extractor-model": "7.30.7", "@microsoft/tsdoc": "~0.15.1", "@microsoft/tsdoc-config": "~0.17.1", "@rushstack/node-core-library": "5.14.0", "@rushstack/rig-package": "0.5.3", "@rushstack/terminal": "0.15.4", "@rushstack/ts-command-line": "5.0.2", "lodash": "~4.17.15", "minimatch": "10.0.3", "resolve": "~1.22.1", "semver": "~7.5.4", "source-map": "~0.6.1", "typescript": "5.8.2" }, "bin": { "api-extractor": "bin/api-extractor" } }, "sha512-IKQ7bHg6f/Io3dQds6r9QPYk4q0OlR9A4nFDtNhUt3UUIhyitbxAqRN1CLjUVtk6IBk3xzyCMOdwwtIXQ7AlGg=="], + + "@microsoft/api-extractor-model": ["@microsoft/api-extractor-model@7.30.7", "", { "dependencies": { "@microsoft/tsdoc": "~0.15.1", "@microsoft/tsdoc-config": "~0.17.1", "@rushstack/node-core-library": "5.14.0" } }, "sha512-TBbmSI2/BHpfR9YhQA7nH0nqVmGgJ0xH0Ex4D99/qBDAUpnhA2oikGmdXanbw9AWWY/ExBYIpkmY8dBHdla3YQ=="], + + "@microsoft/tsdoc": ["@microsoft/tsdoc@0.15.1", "", {}, "sha512-4aErSrCR/On/e5G2hDP0wjooqDdauzEbIq8hIkIe5pXV0rtWJZvdCEKL0ykZxex+IxIwBp0eGeV48hQN07dXtw=="], + + "@microsoft/tsdoc-config": ["@microsoft/tsdoc-config@0.17.1", "", { "dependencies": { "@microsoft/tsdoc": "0.15.1", "ajv": "~8.12.0", "jju": "~1.4.0", "resolve": "~1.22.2" } }, "sha512-UtjIFe0C6oYgTnad4q1QP4qXwLhe6tIpNTRStJ2RZEPIkqQPREAwE5spzVxsdn9UaEMUqhh0AqSx3X4nWAKXWw=="], + "@noble/curves": ["@noble/curves@1.8.1", "", { "dependencies": { "@noble/hashes": "1.7.1" } }, "sha512-warwspo+UYUPep0Q+vtdVB4Ugn8GGQj8iyB3gnRWsztmUHTI3S1nhdiWNsPUGL0vud7JlRRk1XEu7Lq1KGTnMQ=="], "@noble/hashes": ["@noble/hashes@1.7.1", "", {}, "sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ=="], @@ -217,7 +229,7 @@ "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], - "@opensecret/react": ["@opensecret/react@1.4.0", "", { "dependencies": { "@peculiar/x509": "^1.12.2", "@stablelib/base64": "^2.0.0", "@stablelib/chacha20poly1305": "^2.0.0", "@stablelib/random": "^2.0.0", "cbor2": "^1.7.0", "tweetnacl": "^1.0.3", "zod": "^3.23.8" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-21L4V1AWoKTzcMKwe4OrM+sj3BKl5ATMODdFwsh+wdRvmYrG/OXf9AXmO7cP9LfsN7Gb1nj3fTmnwU8Gbx//Kw=="], + "@opensecret/react": ["@opensecret/react@file:../../OpenSecret-SDK", { "dependencies": { "@peculiar/x509": "^1.12.2", "@stablelib/base64": "^2.0.0", "@stablelib/chacha20poly1305": "^2.0.0", "@stablelib/random": "^2.0.0", "cbor2": "^1.7.0", "tweetnacl": "^1.0.3", "zod": "^3.23.8" }, "devDependencies": { "@eslint/js": "^9.13.0", "@noble/curves": "^1.6.0", "@noble/hashes": "^1.5.0", "@types/bun": "latest", "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", "@vitejs/plugin-react": "^4.3.3", "eslint": "^9.13.0", "eslint-plugin-react-hooks": "^5.0.0", "eslint-plugin-react-refresh": "^0.4.14", "globals": "^15.11.0", "openai": "^5.10.2", "prettier": "^3.3.3", "typescript": "~5.6.2", "typescript-eslint": "^8.11.0", "vite": "^5.4.12", "vite-plugin-dts": "^4.3.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }], "@peculiar/asn1-cms": ["@peculiar/asn1-cms@2.3.15", "", { "dependencies": { "@peculiar/asn1-schema": "^2.3.15", "@peculiar/asn1-x509": "^2.3.15", "@peculiar/asn1-x509-attr": "^2.3.15", "asn1js": "^3.0.5", "tslib": "^2.8.1" } }, "sha512-B+DoudF+TCrxoJSTjjcY8Mmu+lbv8e7pXGWrhNp2/EGJp9EEcpzjBCar7puU57sGifyzaRVM03oD5L7t7PghQg=="], @@ -303,8 +315,6 @@ "@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.1.0", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw=="], - "@radix-ui/react-use-effect-event": ["@radix-ui/react-use-effect-event@0.0.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA=="], - "@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.1.0", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw=="], "@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w=="], @@ -319,6 +329,8 @@ "@radix-ui/rect": ["@radix-ui/rect@1.1.0", "", {}, "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg=="], + "@rollup/pluginutils": ["@rollup/pluginutils@5.2.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-qWJ2ZTbmumwiLFomfzTyt5Kng4hwPi9rwCYN4SHb6eaRU1KNO4ccxINHr/VhH4GgPlt1XfSTLX2LBTme8ne4Zw=="], + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.34.8", "", { "os": "android", "cpu": "arm" }, "sha512-q217OSE8DTp8AFHuNHXo0Y86e1wtlfVrXiAlwkIvGRQv9zbc6mE3sjIVfwI8sYUyNxwOg0j/Vm1RKM04JcWLJw=="], "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.34.8", "", { "os": "android", "cpu": "arm64" }, "sha512-Gigjz7mNWaOL9wCggvoK3jEIUUbGul656opstjaUSGC3eT0BM7PofdAJaBfPFWWkXNVAXbaQtC99OCg4sJv70Q=="], @@ -357,6 +369,14 @@ "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.34.8", "", { "os": "win32", "cpu": "x64" }, "sha512-U0FaE5O1BCpZSeE6gBl3c5ObhePQSfk9vDRToMmTkbhCOgW4jqvtS5LGyQ76L1fH8sM0keRp4uDTsbjiUyjk0g=="], + "@rushstack/node-core-library": ["@rushstack/node-core-library@5.14.0", "", { "dependencies": { "ajv": "~8.13.0", "ajv-draft-04": "~1.0.0", "ajv-formats": "~3.0.1", "fs-extra": "~11.3.0", "import-lazy": "~4.0.0", "jju": "~1.4.0", "resolve": "~1.22.1", "semver": "~7.5.4" }, "peerDependencies": { "@types/node": "*" }, "optionalPeers": ["@types/node"] }, "sha512-eRong84/rwQUlATGFW3TMTYVyqL1vfW9Lf10PH+mVGfIb9HzU3h5AASNIw+axnBLjnD0n3rT5uQBwu9fvzATrg=="], + + "@rushstack/rig-package": ["@rushstack/rig-package@0.5.3", "", { "dependencies": { "resolve": "~1.22.1", "strip-json-comments": "~3.1.1" } }, "sha512-olzSSjYrvCNxUFZowevC3uz8gvKr3WTpHQ7BkpjtRpA3wK+T0ybep/SRUMfr195gBzJm5gaXw0ZMgjIyHqJUow=="], + + "@rushstack/terminal": ["@rushstack/terminal@0.15.4", "", { "dependencies": { "@rushstack/node-core-library": "5.14.0", "supports-color": "~8.1.1" }, "peerDependencies": { "@types/node": "*" }, "optionalPeers": ["@types/node"] }, "sha512-OQSThV0itlwVNHV6thoXiAYZlQh4Fgvie2CzxFABsbO2MWQsI4zOh3LRNigYSTrmS+ba2j0B3EObakPzf/x6Zg=="], + + "@rushstack/ts-command-line": ["@rushstack/ts-command-line@5.0.2", "", { "dependencies": { "@rushstack/terminal": "0.15.4", "@types/argparse": "1.0.38", "argparse": "~1.0.9", "string-argv": "~0.3.1" } }, "sha512-+AkJDbu1GFMPIU8Sb7TLVXDv/Q7Mkvx+wAjEl8XiXVVq+p1FmWW6M3LYpJMmoHNckSofeMecgWg5lfMwNAAsEQ=="], + "@stablelib/aead": ["@stablelib/aead@2.0.0", "", {}, "sha512-U/RMANRxbT/ahIpYsPSiFwDFNjADHdnCFfmo09MO1ai2XmerPAOPtMl0qmX7XVvygnACC6ijKDyHBoT2rGyElg=="], "@stablelib/base64": ["@stablelib/base64@2.0.1", "", {}, "sha512-P2z89A7N1ETt6RxgpVdDT2xlg8cnm3n6td0lY9gyK7EiWK3wdq388yFX/hLknkCC0we05OZAD1rfxlQJUbl5VQ=="], @@ -433,6 +453,8 @@ "@tauri-apps/plugin-os": ["@tauri-apps/plugin-os@2.2.1", "", { "dependencies": { "@tauri-apps/api": "^2.0.0" } }, "sha512-cNYpNri2CCc6BaNeB6G/mOtLvg8dFyFQyCUdf2y0K8PIAKGEWdEcu8DECkydU2B+oj4OJihDPD2de5K6cbVl9A=="], + "@types/argparse": ["@types/argparse@1.0.38", "", {}, "sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA=="], + "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], "@types/babel__generator": ["@types/babel__generator@7.6.8", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw=="], @@ -461,8 +483,6 @@ "@types/node": ["@types/node@22.13.4", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-ywP2X0DYtX3y08eFVx5fNIw7/uIv8hYUKgXoK8oayJlLnKcRfEYCxWMVE1XagUdVtCJlZT1AU4LXEABW+L1Peg=="], - "@types/node-fetch": ["@types/node-fetch@2.6.12", "", { "dependencies": { "@types/node": "*", "form-data": "^4.0.0" } }, "sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA=="], - "@types/prop-types": ["@types/prop-types@15.7.14", "", {}, "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ=="], "@types/react": ["@types/react@18.3.18", "", { "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" } }, "sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ=="], @@ -495,16 +515,34 @@ "@vitejs/plugin-react": ["@vitejs/plugin-react@4.3.4", "", { "dependencies": { "@babel/core": "^7.26.0", "@babel/plugin-transform-react-jsx-self": "^7.25.9", "@babel/plugin-transform-react-jsx-source": "^7.25.9", "@types/babel__core": "^7.20.5", "react-refresh": "^0.14.2" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0" } }, "sha512-SCCPBJtYLdE8PX/7ZQAs1QAZ8Jqwih+0VBLum1EGqmCCQal+MIUqLCzj3ZUy8ufbC0cAM4LRlSTm7IQJwWT4ug=="], - "abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="], + "@volar/language-core": ["@volar/language-core@2.4.23", "", { "dependencies": { "@volar/source-map": "2.4.23" } }, "sha512-hEEd5ET/oSmBC6pi1j6NaNYRWoAiDhINbT8rmwtINugR39loROSlufGdYMF9TaKGfz+ViGs1Idi3mAhnuPcoGQ=="], + + "@volar/source-map": ["@volar/source-map@2.4.23", "", {}, "sha512-Z1Uc8IB57Lm6k7q6KIDu/p+JWtf3xsXJqAX/5r18hYOTpJyBn0KXUR8oTJ4WFYOcDzWC9n3IflGgHowx6U6z9Q=="], + + "@volar/typescript": ["@volar/typescript@2.4.23", "", { "dependencies": { "@volar/language-core": "2.4.23", "path-browserify": "^1.0.1", "vscode-uri": "^3.0.8" } }, "sha512-lAB5zJghWxVPqfcStmAP1ZqQacMpe90UrP5RJ3arDyrhy4aCUQqmxPPLB2PWDKugvylmO41ljK7vZ+t6INMTag=="], + + "@vue/compiler-core": ["@vue/compiler-core@3.5.20", "", { "dependencies": { "@babel/parser": "^7.28.3", "@vue/shared": "3.5.20", "entities": "^4.5.0", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "sha512-8TWXUyiqFd3GmP4JTX9hbiTFRwYHgVL/vr3cqhr4YQ258+9FADwvj7golk2sWNGHR67QgmCZ8gz80nQcMokhwg=="], + + "@vue/compiler-dom": ["@vue/compiler-dom@3.5.20", "", { "dependencies": { "@vue/compiler-core": "3.5.20", "@vue/shared": "3.5.20" } }, "sha512-whB44M59XKjqUEYOMPYU0ijUV0G+4fdrHVKDe32abNdX/kJe1NUEMqsi4cwzXa9kyM9w5S8WqFsrfo1ogtBZGQ=="], + + "@vue/compiler-vue2": ["@vue/compiler-vue2@2.7.16", "", { "dependencies": { "de-indent": "^1.0.2", "he": "^1.2.0" } }, "sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A=="], + + "@vue/language-core": ["@vue/language-core@2.2.0", "", { "dependencies": { "@volar/language-core": "~2.4.11", "@vue/compiler-dom": "^3.5.0", "@vue/compiler-vue2": "^2.7.16", "@vue/shared": "^3.5.0", "alien-signals": "^0.4.9", "minimatch": "^9.0.3", "muggle-string": "^0.4.1", "path-browserify": "^1.0.1" }, "peerDependencies": { "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-O1ZZFaaBGkKbsRfnVH1ifOK1/1BUkyK+3SQsfnh6PmMmD4qJcTU8godCeA96jjDRTL6zgnK7YzCHfaUlH2r0Mw=="], + + "@vue/shared": ["@vue/shared@3.5.20", "", {}, "sha512-SoRGP596KU/ig6TfgkCMbXkr4YJ91n/QSdMuqeP5r3hVIYA3CPHUBCc7Skak0EAKV+5lL4KyIh61VA/pK1CIAA=="], "acorn": ["acorn@8.14.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="], "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], - "agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="], - "ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], + "ajv-draft-04": ["ajv-draft-04@1.0.0", "", { "peerDependencies": { "ajv": "^8.5.0" }, "optionalPeers": ["ajv"] }, "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw=="], + + "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], + + "alien-signals": ["alien-signals@0.4.14", "", {}, "sha512-itUAVzhczTmP2U5yX67xVpsbbOiquusbWVyA9N+sy6+r6YVbFkahXvNCeEPWEOMhwDYwbVbGHFkVL03N9I5g+Q=="], + "ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], @@ -523,8 +561,6 @@ "asn1js": ["asn1js@3.0.5", "", { "dependencies": { "pvtsutils": "^1.3.2", "pvutils": "^1.1.3", "tslib": "^2.4.0" } }, "sha512-FVnvrKJwpt9LP2lAMl8qZswRNm3T4q9CON+bxldk2iwk3FFpuwhx2FfinyitizWHsVYyaY+y5JzDR0rCMV5yTQ=="], - "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], - "autoprefixer": ["autoprefixer@10.4.20", "", { "dependencies": { "browserslist": "^4.23.3", "caniuse-lite": "^1.0.30001646", "fraction.js": "^4.3.7", "normalize-range": "^0.1.2", "picocolors": "^1.0.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g=="], "babel-dead-code-elimination": ["babel-dead-code-elimination@1.0.9", "", { "dependencies": { "@babel/core": "^7.23.7", "@babel/parser": "^7.23.6", "@babel/traverse": "^7.23.7", "@babel/types": "^7.23.6" } }, "sha512-JLIhax/xullfInZjtu13UJjaLHDeTzt3vOeomaSUdO/nAMEL/pWC/laKrSvWylXMnVWyL5bpmG9njqBZlUQOdg=="], @@ -543,8 +579,6 @@ "bun-types": ["bun-types@1.2.2", "", { "dependencies": { "@types/node": "*", "@types/ws": "~8.5.10" } }, "sha512-RCbMH5elr9gjgDGDhkTTugA21XtJAy/9jkKe/G3WR2q17VPGhcquf9Sir6uay9iW+7P/BV0CAHA1XlHXMAVKHg=="], - "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], - "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], "camelcase-css": ["camelcase-css@2.0.1", "", {}, "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA=="], @@ -575,14 +609,16 @@ "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], - "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], - "comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="], "commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="], + "compare-versions": ["compare-versions@6.1.1", "", {}, "sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg=="], + "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], + "confbox": ["confbox@0.2.2", "", {}, "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ=="], + "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], @@ -591,14 +627,14 @@ "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], + "de-indent": ["de-indent@1.0.2", "", {}, "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg=="], + "debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], "decode-named-character-reference": ["decode-named-character-reference@1.0.2", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg=="], "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], - "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], - "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], "detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="], @@ -611,8 +647,6 @@ "dlv": ["dlv@1.1.3", "", {}, "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="], - "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], - "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="], "electron-to-chromium": ["electron-to-chromium@1.5.101", "", {}, "sha512-L0ISiQrP/56Acgu4/i/kfPwWSgrzYZUnQrC0+QPFuhqlLP1Ir7qzPPDVS9BcKIyWTRU8+o6CC8dKw38tSWhYIA=="], @@ -621,14 +655,6 @@ "entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], - "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], - - "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], - - "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], - - "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], - "esbuild": ["esbuild@0.21.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.21.5", "@esbuild/android-arm": "0.21.5", "@esbuild/android-arm64": "0.21.5", "@esbuild/android-x64": "0.21.5", "@esbuild/darwin-arm64": "0.21.5", "@esbuild/darwin-x64": "0.21.5", "@esbuild/freebsd-arm64": "0.21.5", "@esbuild/freebsd-x64": "0.21.5", "@esbuild/linux-arm": "0.21.5", "@esbuild/linux-arm64": "0.21.5", "@esbuild/linux-ia32": "0.21.5", "@esbuild/linux-loong64": "0.21.5", "@esbuild/linux-mips64el": "0.21.5", "@esbuild/linux-ppc64": "0.21.5", "@esbuild/linux-riscv64": "0.21.5", "@esbuild/linux-s390x": "0.21.5", "@esbuild/linux-x64": "0.21.5", "@esbuild/netbsd-x64": "0.21.5", "@esbuild/openbsd-x64": "0.21.5", "@esbuild/sunos-x64": "0.21.5", "@esbuild/win32-arm64": "0.21.5", "@esbuild/win32-ia32": "0.21.5", "@esbuild/win32-x64": "0.21.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw=="], "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], @@ -655,9 +681,11 @@ "estree-util-is-identifier-name": ["estree-util-is-identifier-name@3.0.0", "", {}, "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg=="], + "estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], + "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], - "event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="], + "exsolve": ["exsolve@1.0.7", "", {}, "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw=="], "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], @@ -683,26 +711,18 @@ "foreground-child": ["foreground-child@3.3.0", "", { "dependencies": { "cross-spawn": "^7.0.0", "signal-exit": "^4.0.1" } }, "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg=="], - "form-data": ["form-data@4.0.2", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "mime-types": "^2.1.12" } }, "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w=="], - - "form-data-encoder": ["form-data-encoder@1.7.2", "", {}, "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A=="], - - "formdata-node": ["formdata-node@4.4.1", "", { "dependencies": { "node-domexception": "1.0.0", "web-streams-polyfill": "4.0.0-beta.3" } }, "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ=="], - "fraction.js": ["fraction.js@4.3.7", "", {}, "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew=="], + "fs-extra": ["fs-extra@11.3.1", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-eXvGGwZ5CL17ZSwHWd3bbgk7UUpF6IFHtP57NYYakPvHOs8GDgDe5KJI36jIJzDkJ6eJjuzRA8eBQb6SkKue0g=="], + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], - "get-intrinsic": ["get-intrinsic@1.2.7", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "function-bind": "^1.1.2", "get-proto": "^1.0.0", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA=="], - "get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="], - "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], - "get-tsconfig": ["get-tsconfig@4.10.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A=="], "glob": ["glob@10.4.5", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg=="], @@ -713,18 +733,14 @@ "goober": ["goober@2.1.16", "", { "peerDependencies": { "csstype": "^3.0.10" } }, "sha512-erjk19y1U33+XAMe1VTvIONHYoSqE4iS7BYUZfHaqeohLmnC0FdxEh7rQU+6MZ4OajItzjZFSRtVANrQwNq6/g=="], - "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], - "gpt-tokenizer": ["gpt-tokenizer@3.0.1", "", {}, "sha512-5jdaspBq/w4sWw322SvQj1Fku+CN4OAfYZeeEg8U7CWtxBz+zkxZ3h0YOHD43ee+nZYZ5Ud70HRN0ANcdIj4qg=="], + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + "graphemer": ["graphemer@1.4.0", "", {}, "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="], "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], - "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], - - "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], - "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], "hast-util-from-dom": ["hast-util-from-dom@5.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hastscript": "^9.0.0", "web-namespaces": "^2.0.0" } }, "sha512-N+LqofjR2zuzTjCPzyDUdSshy4Ma6li7p/c3pA78uTwzFgENbgbUrm2ugwsOdcjI1muO+o6Dgzp9p8WHtn/39Q=="], @@ -749,16 +765,18 @@ "hastscript": ["hastscript@9.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-parse-selector": "^4.0.0", "property-information": "^6.0.0", "space-separated-tokens": "^2.0.0" } }, "sha512-jzaLBGavEDKHrc5EfFImKN7nZKKBdSLIdGvCwDZ9TfzbF2ffXiov8CKE445L2Z1Ek2t/m4SKQ2j6Ipv7NyUolw=="], + "he": ["he@1.2.0", "", { "bin": { "he": "bin/he" } }, "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="], + "highlight.js": ["highlight.js@11.11.1", "", {}, "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w=="], "html-url-attributes": ["html-url-attributes@3.0.1", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="], - "humanize-ms": ["humanize-ms@1.2.1", "", { "dependencies": { "ms": "^2.0.0" } }, "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ=="], - "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], + "import-lazy": ["import-lazy@4.0.0", "", {}, "sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw=="], + "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], "inline-style-parser": ["inline-style-parser@0.2.4", "", {}, "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q=="], @@ -791,6 +809,8 @@ "jiti": ["jiti@1.21.7", "", { "bin": { "jiti": "bin/jiti.js" } }, "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A=="], + "jju": ["jju@1.4.0", "", {}, "sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA=="], + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], "js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="], @@ -805,18 +825,26 @@ "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], + "jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="], + "katex": ["katex@0.16.21", "", { "dependencies": { "commander": "^8.3.0" }, "bin": { "katex": "cli.js" } }, "sha512-XvqR7FgOHtWupfMiigNzmh+MgUVmDGU2kXZm899ZkPfcuoPuFxyHmXsgATDpFZDAXCI8tvinaVcDo8PIIJSo4A=="], "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], + "kolorist": ["kolorist@1.8.0", "", {}, "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ=="], + "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], "lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="], "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], + "local-pkg": ["local-pkg@1.1.2", "", { "dependencies": { "mlly": "^1.7.4", "pkg-types": "^2.3.0", "quansync": "^0.2.11" } }, "sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A=="], + "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], + "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], + "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], "longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="], @@ -829,9 +857,9 @@ "lucide-react": ["lucide-react@0.436.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc" } }, "sha512-N292bIxoqm1aObAg0MzFtvhYwgQE6qnIOWx/GLj5ONgcTPH6N0fD9bVq/GfdeC9ZORBXozt/XeEKDpiB3x3vlQ=="], - "markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="], + "magic-string": ["magic-string@0.30.18", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-yi8swmWbO17qHhwIBNeeZxTceJMeBvWJaId6dyvTSOwTipqeHhMhOrz6513r1sOKnpvQ7zkhlG8tPrpilwTxHQ=="], - "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + "markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="], "mdast-util-find-and-replace": ["mdast-util-find-and-replace@3.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "escape-string-regexp": "^5.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg=="], @@ -929,26 +957,22 @@ "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], - "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], - - "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], - "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], "minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], + "mlly": ["mlly@1.8.0", "", { "dependencies": { "acorn": "^8.15.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.1" } }, "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g=="], + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "muggle-string": ["muggle-string@0.4.1", "", {}, "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ=="], + "mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="], "nanoid": ["nanoid@3.3.8", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w=="], "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], - "node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="], - - "node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], - "node-releases": ["node-releases@2.0.19", "", {}, "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw=="], "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], @@ -959,7 +983,7 @@ "object-hash": ["object-hash@3.0.0", "", {}, "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw=="], - "openai": ["openai@4.85.1", "", { "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", "abort-controller": "^3.0.0", "agentkeepalive": "^4.2.1", "form-data-encoder": "1.7.2", "formdata-node": "^4.3.2", "node-fetch": "^2.6.7" }, "peerDependencies": { "ws": "^8.18.0", "zod": "^3.23.8" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-jkX2fntHljUvSH3MkWh4jShl10oNkb+SsCj4auKlbu2oF4KWAnmHLNR5EpnUHK1ZNW05Rp0fjbJzYwQzMsH8ZA=="], + "openai": ["openai@5.16.0", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.23.8" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-hoEH8ZNvg1HXjU9mp88L/ZH8O082Z8r6FHCXGiWAzVRrEv443aI57qhch4snu07yQydj+AUAWLenAiBXhu89Tw=="], "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], @@ -975,6 +999,8 @@ "parse5": ["parse5@7.2.1", "", { "dependencies": { "entities": "^4.5.0" } }, "sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ=="], + "path-browserify": ["path-browserify@1.0.1", "", {}, "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="], + "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], @@ -983,6 +1009,8 @@ "path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], + "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], "picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], @@ -991,6 +1019,8 @@ "pirates": ["pirates@4.0.6", "", {}, "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg=="], + "pkg-types": ["pkg-types@2.3.0", "", { "dependencies": { "confbox": "^0.2.2", "exsolve": "^1.0.7", "pathe": "^2.0.3" } }, "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig=="], + "postcss": ["postcss@8.5.2", "", { "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-MjOadfU3Ys9KYoX0AdkBlFEF1Vx37uCCeN4ZHnmwm9FfpbsGWMZeBLMmmpY+6Ocqod7mkdZ0DT31OlbsFrLlkA=="], "postcss-import": ["postcss-import@15.1.0", "", { "dependencies": { "postcss-value-parser": "^4.0.0", "read-cache": "^1.0.0", "resolve": "^1.1.7" }, "peerDependencies": { "postcss": "^8.0.0" } }, "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew=="], @@ -1017,6 +1047,8 @@ "pvutils": ["pvutils@1.1.3", "", {}, "sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ=="], + "quansync": ["quansync@0.2.11", "", {}, "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA=="], + "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], "react": ["react@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ=="], @@ -1057,6 +1089,8 @@ "remark-stringify": ["remark-stringify@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-to-markdown": "^2.0.0", "unified": "^11.0.0" } }, "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw=="], + "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], + "resolve": ["resolve@1.22.10", "", { "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w=="], "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], @@ -1079,10 +1113,16 @@ "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], "space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="], + "sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="], + + "string-argv": ["string-argv@0.3.2", "", {}, "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q=="], + "string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], "string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], @@ -1119,8 +1159,6 @@ "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], - "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], - "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="], "trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="], @@ -1143,6 +1181,8 @@ "typescript-eslint": ["typescript-eslint@8.24.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.24.0", "@typescript-eslint/parser": "8.24.0", "@typescript-eslint/utils": "8.24.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-/lmv4366en/qbB32Vz5+kCNZEMf6xYHwh1z48suBwZvAtnXKbP+YhGe8OLE2BqC67LMqKkCNLtjejdwsdW6uOQ=="], + "ufo": ["ufo@1.6.1", "", {}, "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA=="], + "undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="], "unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="], @@ -1161,6 +1201,8 @@ "unist-util-visit-parents": ["unist-util-visit-parents@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw=="], + "universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], + "unplugin": ["unplugin@2.2.0", "", { "dependencies": { "acorn": "^8.14.0", "webpack-virtual-modules": "^0.6.2" } }, "sha512-m1ekpSwuOT5hxkJeZGRxO7gXbXT3gF26NjQ7GdVHoLoF8/nopLcd/QfPigpCy7i51oFHiRJg/CyHhj4vs2+KGw=="], "update-browserslist-db": ["update-browserslist-db@1.1.2", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-PPypAm5qvlD7XMZC3BujecnaOxwhrtoFR+Dqkk5Aa/6DssiH0ibKoketaj9w8LP7Bont1rYeoV5plxD7RTEPRg=="], @@ -1185,16 +1227,14 @@ "vite": ["vite@5.4.14", "", { "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", "rollup": "^4.20.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["@types/node", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser"], "bin": { "vite": "bin/vite.js" } }, "sha512-EK5cY7Q1D8JNhSaPKVK4pwBFvaTmZxEnoKXLG/U9gmdDcihQGNzFlgIvaxezFR4glP1LsuiedwMBqCXH3wZccA=="], - "web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="], + "vite-plugin-dts": ["vite-plugin-dts@4.5.4", "", { "dependencies": { "@microsoft/api-extractor": "^7.50.1", "@rollup/pluginutils": "^5.1.4", "@volar/typescript": "^2.4.11", "@vue/language-core": "2.2.0", "compare-versions": "^6.1.1", "debug": "^4.4.0", "kolorist": "^1.8.0", "local-pkg": "^1.0.0", "magic-string": "^0.30.17" }, "peerDependencies": { "typescript": "*", "vite": "*" }, "optionalPeers": ["vite"] }, "sha512-d4sOM8M/8z7vRXHHq/ebbblfaxENjogAAekcfcDCCwAyvGqnPrc7f4NZbvItS+g4WTgerW0xDwSz5qz11JT3vg=="], - "web-streams-polyfill": ["web-streams-polyfill@4.0.0-beta.3", "", {}, "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug=="], + "vscode-uri": ["vscode-uri@3.1.0", "", {}, "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ=="], - "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], + "web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="], "webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="], - "whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], - "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], @@ -1223,45 +1263,43 @@ "@humanfs/node/@humanwhocodes/retry": ["@humanwhocodes/retry@0.3.1", "", {}, "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA=="], - "@radix-ui/react-menu/@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.2", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-collection": "1.1.2", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-direction": "1.1.0", "@radix-ui/react-id": "1.1.0", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-controllable-state": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-zgMQWkNO169GtGqRvYrzb0Zf8NhMHS2DuEB/TiEmVnpr5OqPU3i8lfbxaAmC2J/KYuIQxyoQQ6DxepyXp61/xw=="], + "@microsoft/api-extractor/minimatch": ["minimatch@10.0.3", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw=="], - "@radix-ui/react-roving-focus/@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="], + "@microsoft/api-extractor/semver": ["semver@7.5.4", "", { "dependencies": { "lru-cache": "^6.0.0" }, "bin": { "semver": "bin/semver.js" } }, "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA=="], - "@radix-ui/react-roving-focus/@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw=="], + "@microsoft/api-extractor/typescript": ["typescript@5.8.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ=="], - "@radix-ui/react-roving-focus/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="], + "@microsoft/tsdoc-config/ajv": ["ajv@8.12.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2", "uri-js": "^4.2.2" } }, "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA=="], - "@radix-ui/react-roving-focus/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], + "@opensecret/react/@types/bun": ["@types/bun@1.2.21", "", { "dependencies": { "bun-types": "1.2.21" } }, "sha512-NiDnvEqmbfQ6dmZ3EeUO577s4P5bf4HCTXtI6trMc6f6RzirY5IrF3aIookuSpyslFzrnvv2lmEWv5HyC1X79A=="], - "@radix-ui/react-roving-focus/@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw=="], + "@opensecret/react/@types/react": ["@types/react@19.1.11", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-lr3jdBw/BGj49Eps7EvqlUaoeA0xpj3pc0RoJkHpYaCHkVK7i28dKyImLQb3JVlqs3aYSXf7qYuWOW/fgZnTXQ=="], - "@radix-ui/react-roving-focus/@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="], + "@opensecret/react/@types/react-dom": ["@types/react-dom@19.1.8", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-xG7xaBMJCpcK0RpN8jDbAACQo54ycO6h4dSSmgv8+fu6ZIAdANkx/WsawASUjVXYfy+J9AbUpRMNNEsXCDfDBQ=="], - "@radix-ui/react-roving-focus/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], + "@opensecret/react/openai": ["openai@5.15.0", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.23.8" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-kcUdws8K/A8m02I+IqFBwO51gS+87GP89yWEufGbzEi8anBz4FB/bti2QxaJdGwwY4mwJGzx85XO7TuL/Tpu1w=="], - "@radix-ui/react-roving-focus/@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="], + "@opensecret/react/typescript": ["typescript@5.6.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw=="], - "@radix-ui/react-roving-focus/@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="], + "@rollup/pluginutils/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], - "@radix-ui/react-tabs/@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="], + "@rushstack/node-core-library/ajv": ["ajv@8.13.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2", "uri-js": "^4.4.1" } }, "sha512-PRA911Blj99jR5RMeTunVbNXMF6Lp4vZXnk5GQjcnUWUTsrXtekg/pnmFFI2u/I36Y/2bITGS30GZCXei6uNkA=="], - "@radix-ui/react-tabs/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], + "@rushstack/node-core-library/semver": ["semver@7.5.4", "", { "dependencies": { "lru-cache": "^6.0.0" }, "bin": { "semver": "bin/semver.js" } }, "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA=="], - "@radix-ui/react-tabs/@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw=="], + "@rushstack/terminal/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], - "@radix-ui/react-tabs/@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="], + "@rushstack/ts-command-line/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], - "@radix-ui/react-tabs/@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.5", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ=="], - - "@radix-ui/react-tabs/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], + "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], - "@radix-ui/react-tabs/@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="], + "@typescript-eslint/typescript-estree/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="], - "@radix-ui/react-use-effect-event/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="], + "@vue/compiler-core/@babel/parser": ["@babel/parser@7.28.3", "", { "dependencies": { "@babel/types": "^7.28.2" }, "bin": "./bin/babel-parser.js" }, "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA=="], - "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + "@vue/language-core/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], - "@typescript-eslint/typescript-estree/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="], + "ajv-formats/ajv": ["ajv@8.13.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2", "uri-js": "^4.4.1" } }, "sha512-PRA911Blj99jR5RMeTunVbNXMF6Lp4vZXnk5GQjcnUWUTsrXtekg/pnmFFI2u/I36Y/2bITGS30GZCXei6uNkA=="], "chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], @@ -1271,9 +1309,13 @@ "katex/commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="], + "magic-string/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + "mdast-util-find-and-replace/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], - "openai/@types/node": ["@types/node@18.19.76", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-yvR7Q9LdPz2vGpmpJX5LolrgRdWvB67MJKDPSgIIzpFbaf9a1j/f5DnLp5VDyHGMR0QZHlTr1afsD87QCXFHKw=="], + "mlly/acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], + + "mlly/pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="], "parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], @@ -1295,29 +1337,27 @@ "wrap-ansi-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "@radix-ui/react-roving-focus/@radix-ui/react-collection/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], - - "@radix-ui/react-roving-focus/@radix-ui/react-id/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="], + "@microsoft/api-extractor/semver/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], - "@radix-ui/react-roving-focus/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + "@microsoft/tsdoc-config/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], - "@radix-ui/react-roving-focus/@radix-ui/react-use-controllable-state/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="], + "@opensecret/react/@types/bun/bun-types": ["bun-types@1.2.21", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-sa2Tj77Ijc/NTLS0/Odjq/qngmEPZfbfnOERi0KRUYhT9R8M4VBioWVmMWE5GrYbKMc+5lVybXygLdibHaqVqw=="], - "@radix-ui/react-tabs/@radix-ui/react-id/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="], + "@rushstack/node-core-library/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], - "@radix-ui/react-tabs/@radix-ui/react-presence/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="], + "@rushstack/node-core-library/semver/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], - "@radix-ui/react-tabs/@radix-ui/react-presence/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="], + "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], - "@radix-ui/react-tabs/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + "@vue/compiler-core/@babel/parser/@babel/types": ["@babel/types@7.28.2", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ=="], - "@radix-ui/react-tabs/@radix-ui/react-use-controllable-state/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="], + "@vue/language-core/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], - "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], + "ajv-formats/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], "glob/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], - "openai/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + "mlly/pkg-types/confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="], "string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], @@ -1371,6 +1411,12 @@ "wrap-ansi-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "@radix-ui/react-tabs/@radix-ui/react-primitive/@radix-ui/react-slot/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="], + "@microsoft/api-extractor/semver/lru-cache/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], + + "@rushstack/node-core-library/semver/lru-cache/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], + + "@vue/compiler-core/@babel/parser/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], + + "@vue/compiler-core/@babel/parser/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="], } } diff --git a/frontend/package.json b/frontend/package.json index b2d1880cd..d17dc98b4 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -16,7 +16,7 @@ "mdast-util-gfm-autolink-literal": "2.0.0" }, "dependencies": { - "@opensecret/react": "1.4.0", + "@opensecret/react": "file:../../OpenSecret-SDK", "@radix-ui/react-alert-dialog": "^1.1.1", "@radix-ui/react-avatar": "^1.1.0", "@radix-ui/react-dialog": "^1.1.1", @@ -38,7 +38,7 @@ "clsx": "^2.1.1", "gpt-tokenizer": "^3.0.1", "lucide-react": "^0.436.0", - "openai": "^4.56.1", + "openai": "5.16.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-markdown": "^9.0.1", diff --git a/frontend/src/ai/OpenAIContext.tsx b/frontend/src/ai/OpenAIContext.tsx index 72fa43a59..d73222e83 100644 --- a/frontend/src/ai/OpenAIContext.tsx +++ b/frontend/src/ai/OpenAIContext.tsx @@ -1,5 +1,6 @@ +import React, { useContext, useMemo } from "react"; import OpenAI from "openai"; -import { useOpenSecret } from "@opensecret/react"; +import { OpenSecretContext, type OpenSecretContextType } from "@opensecret/react"; import { OpenAIContext } from "./OpenAIContextDef"; export const OpenAIProvider = ({ children }: { children: React.ReactNode }) => { @@ -8,24 +9,28 @@ export const OpenAIProvider = ({ children }: { children: React.ReactNode }) => { throw new Error("VITE_OPEN_SECRET_API_URL must be set"); } - const { aiCustomFetch } = useOpenSecret(); + const { aiCustomFetch } = useContext(OpenSecretContext as React.Context); const access_token = window.localStorage.getItem("access_token"); - // If we're not logged in we can't set up openai - if (!access_token) { - return {children}; - } + // Memoize the OpenAI client to prevent recreating it on every render + const openai = useMemo(() => { + // If we're not logged in or don't have aiCustomFetch, return undefined + if (!access_token || !aiCustomFetch) { + return undefined; + } + - // Custom fetch function that allows us to refresh the access token - const openai = new OpenAI({ - baseURL: `${url}/v1/`, - dangerouslyAllowBrowser: true, - apiKey: "not-a-real-api-key", - defaultHeaders: { - "Accept-Encoding": "identity" - }, - fetch: aiCustomFetch - }); + // Custom fetch function that allows us to refresh the access token + return new OpenAI({ + baseURL: `${url}/v1/`, + dangerouslyAllowBrowser: true, + apiKey: "not-a-real-api-key", + defaultHeaders: { + "Accept-Encoding": "identity" + }, + fetch: aiCustomFetch as any + }); + }, [url, aiCustomFetch, access_token]); return {children}; }; diff --git a/frontend/src/hooks/useChatSession.ts b/frontend/src/hooks/useChatSession.ts index c533cbf15..d169bcd31 100644 --- a/frontend/src/hooks/useChatSession.ts +++ b/frontend/src/hooks/useChatSession.ts @@ -101,43 +101,41 @@ export function useChatSession( const streamAssistant = useCallback( async (messages: ChatMessage[]): Promise => { - // Create new abort controller for this stream + // Legacy path using Chat Completions (kept for backward compatibility) const abortController = new AbortController(); abortControllerRef.current = abortController; try { - const stream = openai.beta.chat.completions.stream({ + const stream = await openai.chat.completions.create({ model, - messages: messages as Parameters< - typeof openai.beta.chat.completions.stream - >[0]["messages"], + messages: messages as Parameters[0]["messages"], stream: true - }); + } as any); let fullResponse = ""; setCurrentStreamingMessage(""); - for await (const chunk of stream) { - // Check if we should abort + for await (const chunk of stream as any) { if (abortController.signal.aborted) { - stream.controller.abort(); - throw new Error("Stream aborted"); + break; + } + const content = chunk.choices?.[0]?.delta?.content || ""; + if (content) { + fullResponse += content; + setCurrentStreamingMessage(fullResponse); } - - const content = chunk.choices[0]?.delta?.content || ""; - fullResponse += content; - setCurrentStreamingMessage(fullResponse); } - await stream.finalChatCompletion(); setCurrentStreamingMessage(undefined); + if (abortController.signal.aborted) { + throw new Error("Stream aborted"); + } return fullResponse; } catch (error) { if (abortController.signal.aborted) { throw new Error("Stream aborted"); } - // Handle streaming errors from OpenAI let errorMessage = "An error occurred while streaming the response."; if (error instanceof Error) { if ( @@ -159,7 +157,6 @@ export function useChatSession( errorMessage = error.message; } } - setStreamingError(errorMessage); throw error; } finally { @@ -252,45 +249,104 @@ export function useChatSession( }); try { - // Start title generation in background if needed - let titlePromise: Promise | undefined; - if (chat.title === "New Chat") { - titlePromise = generateTitle(newMessages, openai, queryClient); - // Update title in UI as soon as it's ready - titlePromise.then((generatedTitle) => { - setOptimisticChat((prev) => { - if (!prev) return prev; - return { ...prev, title: generatedTitle }; + // const useResponses = import.meta.env.VITE_USE_RESPONSES === "true"; + const useResponses = true; + + if (useResponses) { + // Responses API path (no client-side title generation, server persists/title) + const abortController = new AbortController(); + abortControllerRef.current = abortController; + + setCurrentStreamingMessage(""); + + let accumulated = ""; + const inputText = typeof finalContent === "string" ? finalContent : String(finalContent); + const instructions = systemPrompt?.trim() || undefined; + + let stream: any; + try { + stream = await openai.responses.create({ + model, + input: inputText, + instructions, + stream: true + } as any); + } catch (error) { + console.error("Error creating stream:", error); + if (error instanceof Error) { + console.error("Error details:", error.message, error.stack); + } + setStreamingError("Failed to create response stream"); + setPhase("idle"); + processingRef.current = false; + return; + } + + try { + for await (const event of stream) { + if (abortController.signal.aborted) break; + + // Handle different event types based on OpenAI Responses API spec + if (event.type === "response.created") { + // Response started, could capture the ID here if needed + } else if (event.type === "response.output_item.added") { + // New output item started + } else if (event.type === "response.output_text.delta") { + const delta = event.delta || ""; + if (delta) { + accumulated += delta; + setCurrentStreamingMessage(accumulated); + } + } else if (event.type === "response.output_text.done") { + // Text output completed + } else if (event.type === "response.completed" || event.type === "response.done") { + // Response completed + const finalMessages = [ + ...newMessages, + { role: "assistant", content: accumulated } as ChatMessage + ]; + setOptimisticChat((prev) => ({ ...prev!, messages: finalMessages })); + setCurrentStreamingMessage(undefined); + setPhase("idle"); + queryClient.invalidateQueries({ queryKey: ["chatHistory"] }); + break; + } else if (event.type === "error") { + setStreamingError(event.message || "Streaming error"); + break; + } + } + } finally { + // Clean up but don't throw in finally block + if (abortController.signal.aborted) { + console.log("Stream was aborted"); + } + } + } else { + // Legacy completions path + // Start title generation in background if needed + throw new Error("Completions API is deprecated"); + let titlePromise: Promise | undefined; + if (chat.title === "New Chat") { + titlePromise = generateTitle(newMessages, openai, queryClient); + titlePromise.then((generatedTitle) => { + setOptimisticChat((prev) => { + if (!prev) return prev; + return { ...prev, title: generatedTitle }; + }); }); - }); - } + } - // Stream assistant response - const assistantResponse = await streamAssistant(newMessages); - - // Add assistant message - const finalMessages = [ - ...newMessages, - { role: "assistant", content: assistantResponse } as ChatMessage - ]; - - // Update optimistic state with assistant message - setOptimisticChat((prev) => ({ - ...prev!, - messages: finalMessages - })); - - // Wait for title generation if it was started - const title = titlePromise ? await titlePromise : chat.title; - - // Persist to backend - setPhase("persisting"); - await persistMutation.mutateAsync({ - id: chatId, - title, - model, - messages: finalMessages - }); + const assistantResponse = await streamAssistant(newMessages); + const finalMessages = [ + ...newMessages, + { role: "assistant", content: assistantResponse } as ChatMessage + ]; + setOptimisticChat((prev) => ({ ...prev!, messages: finalMessages })); + + const title = titlePromise ? await titlePromise : chat.title; + setPhase("persisting"); + await persistMutation.mutateAsync({ id: chatId, title, model, messages: finalMessages }); + } } catch (error) { setPhase("idle"); processingRef.current = false; @@ -380,7 +436,7 @@ async function generateTitle( const userContent = messageText.slice(0, 500); // Reduced to 500 chars to optimize token usage // Use the OpenAI API to generate a concise title - use the default model - const stream = openai.beta.chat.completions.stream({ + const stream = await openai.chat.completions.create({ model: DEFAULT_MODEL_ID, // Use the default model instead of user selected model messages: [ { @@ -396,17 +452,14 @@ async function generateTitle( temperature: 0.7, max_tokens: 15, // Keep response very short stream: true - }); + } as any); let generatedTitle = ""; - for await (const chunk of stream) { - const content = chunk.choices[0]?.delta?.content || ""; - generatedTitle += content; + for await (const chunk of stream as any) { + const content = chunk.choices?.[0]?.delta?.content || ""; + if (content) generatedTitle += content; } - // Get the final completion - await stream.finalChatCompletion(); - // Remove quotes if present and limit length const cleanTitle = generatedTitle .replace(/^["']|["']$/g, "") // Remove leading/trailing quotes From cac2ff692867d26f1d7affb405c5b874d4deb03c Mon Sep 17 00:00:00 2001 From: Paul Miller Date: Wed, 27 Aug 2025 12:50:15 -0500 Subject: [PATCH 3/5] hacky wip chats that works! --- UPDATE_TO_RESPONSES_API.md | 43 +++- frontend/src/hooks/useChatSession.ts | 270 ++++++--------------- frontend/src/routes/_auth.chat.$chatId.tsx | 9 +- frontend/src/routes/index.tsx | 5 +- frontend/src/state/LocalStateContext.tsx | 129 +++++++++- frontend/src/state/LocalStateContextDef.ts | 1 + 6 files changed, 233 insertions(+), 224 deletions(-) diff --git a/UPDATE_TO_RESPONSES_API.md b/UPDATE_TO_RESPONSES_API.md index 7c8cca6a4..16d5ab040 100644 --- a/UPDATE_TO_RESPONSES_API.md +++ b/UPDATE_TO_RESPONSES_API.md @@ -138,13 +138,42 @@ Code changes (initial): --- ## Future Work -1) Load and migrate legacy chats to Responses - - Provide a migration UI: select legacy thread → create new Responses thread → seed context - - Mark legacy items as migrated; keep references for back‑links -2) Unify chat interface (single‑page UX) - - Start on home page; push `chatId` into URL - - Avoid full page remount across chat navigation - - Smoothly switch between threads without losing stream state + +### 1. Load and migrate legacy chats to Responses +- Provide a migration UI: select legacy thread → create new Responses thread → seed context +- Mark legacy items as migrated; keep references for back‑links + +### 2. Improve Chat Handoff (Medium Refactor) +**Problem:** Currently when starting a chat from home route, we create a local chat ID, store prompt/images in context, navigate to chat route, then chat route reads the handoff data and starts streaming. This is clunky and causes unnecessary route transitions. + +**Solution:** Make the home route (`/`) handle both empty and active chat states: +- When user sends first message from home: + - Start streaming immediately in place (no navigation) + - Get response ID from server via `response.created` event + - Use `window.history.replaceState` to update URL to `/chat/[id]` without page reload + - Continue streaming in the same component instance +- Benefits: + - Delete handoff logic (userPrompt, systemPrompt, userImages context state) + - Delete duplicate chat rendering logic between home and chat routes + - Smoother UX with no visible navigation flash + +### 3. Unified Chat Component (Larger Refactor) +**Problem:** Duplicate logic between home and chat routes for rendering and managing chats. + +**Solution:** Create a single `` component that works for both new and existing chats: +- Home route renders: `` +- Chat route renders: `` +- Component handles: + - If `chatId` is null and message sent → create response, update URL via `replaceState` + - If `chatId` exists → load from Responses API or legacy storage + - All streaming, rendering, and state management in one place +- Benefits: + - Single source of truth for chat UI logic + - Delete all handoff/navigation code + - Cleaner separation of concerns + - With Responses API, we ALWAYS have server state - no need for complex local persistence + +**Note:** Once fully on Responses API, we can delete the completions API code entirely. Every chat exists on the server, we only need to support loading (not appending to) legacy chats from storage. --- diff --git a/frontend/src/hooks/useChatSession.ts b/frontend/src/hooks/useChatSession.ts index d169bcd31..905752ac8 100644 --- a/frontend/src/hooks/useChatSession.ts +++ b/frontend/src/hooks/useChatSession.ts @@ -1,9 +1,10 @@ import { useState, useCallback, useEffect, useMemo, useRef } from "react"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; -import { Chat, ChatMessage, DEFAULT_MODEL_ID } from "@/state/LocalStateContext"; +import { Chat, ChatMessage } from "@/state/LocalStateContext"; +// import { DEFAULT_MODEL_ID } from "@/state/LocalStateContext"; // Commented out - used in legacy code import { ChatContentPart } from "@/state/LocalStateContextDef"; import { fileToDataURL } from "@/utils/file"; -import { BillingStatus } from "@/billing/billingApi"; +// import { BillingStatus } from "@/billing/billingApi"; // Commented out - used in legacy title generation import { MODEL_CONFIG } from "@/components/ModelSelector"; type ChatPhase = "idle" | "streaming" | "persisting"; @@ -13,6 +14,7 @@ interface UseChatSessionOptions { persistChat: (chat: Chat) => Promise; openai: ReturnType; model: string; + onThreadCreated?: (threadId: string) => void; // Callback when a new thread is created } export function useChatSession( @@ -21,7 +23,7 @@ export function useChatSession( onImageConversionError?: (failedCount: number) => void; } ) { - const { getChatById, persistChat, openai, model, onImageConversionError } = options; + const { getChatById, persistChat, openai, model, onImageConversionError, onThreadCreated } = options; const queryClient = useQueryClient(); const [phase, setPhase] = useState("idle"); const [optimisticChat, setOptimisticChat] = useState(null); @@ -29,6 +31,7 @@ export function useChatSession( const [streamingError, setStreamingError] = useState(null); const processingRef = useRef(false); const abortControllerRef = useRef(null); + const threadIdRef = useRef(null); // Track the actual thread ID // Query the chat from backend const { data: serverChat, isPending } = useQuery({ @@ -50,6 +53,15 @@ export function useChatSession( setCurrentStreamingMessage(undefined); setStreamingError(null); processingRef.current = false; + + // Reset thread ID when navigating to a different chat + // But keep it if we're loading an existing thread + if (chatId === "new") { + threadIdRef.current = null; + } else if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(chatId)) { + // This is an existing thread ID + threadIdRef.current = chatId; + } }, [chatId]); // Cleanup on unmount @@ -98,76 +110,6 @@ export function useChatSession( }, [optimisticChat, serverChat, chatId] ); - - const streamAssistant = useCallback( - async (messages: ChatMessage[]): Promise => { - // Legacy path using Chat Completions (kept for backward compatibility) - const abortController = new AbortController(); - abortControllerRef.current = abortController; - - try { - const stream = await openai.chat.completions.create({ - model, - messages: messages as Parameters[0]["messages"], - stream: true - } as any); - - let fullResponse = ""; - setCurrentStreamingMessage(""); - - for await (const chunk of stream as any) { - if (abortController.signal.aborted) { - break; - } - const content = chunk.choices?.[0]?.delta?.content || ""; - if (content) { - fullResponse += content; - setCurrentStreamingMessage(fullResponse); - } - } - - setCurrentStreamingMessage(undefined); - if (abortController.signal.aborted) { - throw new Error("Stream aborted"); - } - return fullResponse; - } catch (error) { - if (abortController.signal.aborted) { - throw new Error("Stream aborted"); - } - - let errorMessage = "An error occurred while streaming the response."; - if (error instanceof Error) { - if ( - error.message.includes("stream_error") || - error.message.includes("Stream processing error") - ) { - errorMessage = "Stream processing error. Please try again."; - } else if (error.message.includes("401")) { - errorMessage = "Authentication error. Please check your API key."; - } else if (error.message.includes("429")) { - errorMessage = "Rate limit exceeded. Please wait a moment and try again."; - } else if ( - error.message.includes("500") || - error.message.includes("502") || - error.message.includes("503") - ) { - errorMessage = "Server error. The AI service is temporarily unavailable."; - } else { - errorMessage = error.message; - } - } - setStreamingError(errorMessage); - throw error; - } finally { - if (abortControllerRef.current === abortController) { - abortControllerRef.current = null; - } - } - }, - [openai, model] - ); - const appendUserMessage = useCallback( async ( content: string, @@ -265,12 +207,29 @@ export function useChatSession( let stream: any; try { - stream = await openai.responses.create({ + // Use the thread ID if we have it (for follow-up messages in the same session) + // or use the chatId if it's a valid UUID (for loading existing threads) + const effectiveThreadId = threadIdRef.current || + (chatId !== "new" && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(chatId) ? chatId : null); + + const requestParams: any = { model, input: inputText, instructions, - stream: true - } as any); + stream: true, + store: true // Ensure messages are persisted + }; + + // If we have a thread ID, this is a continuation + if (effectiveThreadId) { + requestParams.previous_response_id = effectiveThreadId; + console.log("Continuing thread with previous_response_id:", effectiveThreadId); + } else { + console.log("Starting new thread (no previous_response_id)"); + } + + console.log("Creating response with params:", requestParams); + stream = await openai.responses.create(requestParams); } catch (error) { console.error("Error creating stream:", error); if (error instanceof Error) { @@ -282,13 +241,27 @@ export function useChatSession( return; } + let responseId: string | null = null; + try { for await (const event of stream) { if (abortController.signal.aborted) break; // Handle different event types based on OpenAI Responses API spec if (event.type === "response.created") { - // Response started, could capture the ID here if needed + // Capture the response ID + responseId = event.response?.id; + console.log("Response created with ID:", responseId); + + // For new chats, store the thread ID for follow-up messages + if (chatId === "new" && !threadIdRef.current && responseId) { + console.log("New thread created with ID:", responseId); + threadIdRef.current = responseId; + // Call the callback to update the URL via React Router + if (onThreadCreated) { + onThreadCreated(responseId); + } + } } else if (event.type === "response.output_item.added") { // New output item started } else if (event.type === "response.output_text.delta") { @@ -308,6 +281,23 @@ export function useChatSession( setOptimisticChat((prev) => ({ ...prev!, messages: finalMessages })); setCurrentStreamingMessage(undefined); setPhase("idle"); + + // Store messages locally for display purposes (Responses API maintains real context) + // This is a temporary solution until we have a thread history endpoint + if (threadIdRef.current) { + try { + localStorage.setItem( + `responses_chat_${threadIdRef.current}`, + JSON.stringify({ + messages: finalMessages, + lastUpdated: Date.now() + }) + ); + } catch (error) { + console.error("Failed to store messages locally:", error); + } + } + queryClient.invalidateQueries({ queryKey: ["chatHistory"] }); break; } else if (event.type === "error") { @@ -322,30 +312,8 @@ export function useChatSession( } } } else { - // Legacy completions path - // Start title generation in background if needed - throw new Error("Completions API is deprecated"); - let titlePromise: Promise | undefined; - if (chat.title === "New Chat") { - titlePromise = generateTitle(newMessages, openai, queryClient); - titlePromise.then((generatedTitle) => { - setOptimisticChat((prev) => { - if (!prev) return prev; - return { ...prev, title: generatedTitle }; - }); - }); - } - - const assistantResponse = await streamAssistant(newMessages); - const finalMessages = [ - ...newMessages, - { role: "assistant", content: assistantResponse } as ChatMessage - ]; - setOptimisticChat((prev) => ({ ...prev!, messages: finalMessages })); - - const title = titlePromise ? await titlePromise : chat.title; - setPhase("persisting"); - await persistMutation.mutateAsync({ id: chatId, title, model, messages: finalMessages }); + // Legacy completions path is deprecated + throw new Error("Completions API is deprecated - please use Responses API"); } } catch (error) { setPhase("idle"); @@ -363,7 +331,7 @@ export function useChatSession( processingRef.current = false; } }, - [chat, model, phase, streamAssistant, persistMutation, chatId, openai, queryClient] + [chat, model, phase, persistMutation, chatId, openai, queryClient] ); return { @@ -371,105 +339,7 @@ export function useChatSession( phase, currentStreamingMessage, appendUserMessage, - streamAssistant, streamingError, isPending }; } - -// Helper to generate chat title -async function generateTitle( - messages: ChatMessage[], - openai: ReturnType, - queryClient: ReturnType -): Promise { - // Helper function to check if the user is on a free plan - function isUserOnFreePlan(): boolean { - try { - const billingStatus = queryClient.getQueryData(["billingStatus"]) as - | BillingStatus - | undefined; - - return ( - !billingStatus || - !billingStatus.product_name || - billingStatus.product_name.toLowerCase().includes("free") - ); - } catch (error) { - console.log("Error checking billing status, defaulting to free plan", error); - return true; // Default to free plan if there's an error - } - } - - const userMessage = messages.find((m) => m.role === "user"); - if (!userMessage) return "New Chat"; - - let messageText = "New Chat"; - - if (typeof userMessage.content === "string") { - messageText = userMessage.content; - } else if (Array.isArray(userMessage.content)) { - // Find the first text part safely - const textPart = userMessage.content.find( - (part): part is { type: "text"; text: string } => - part.type === "text" && "text" in part && typeof part.text === "string" - ); - if (textPart) { - messageText = textPart.text; - } - } - - // Simple title generation - truncate first message to 50 chars - const simpleTitleFromMessage = messageText.slice(0, 50).trim(); - - // For free plan users, just use the simple title - // For paid plans, try to generate AI title - if (isUserOnFreePlan()) { - console.log("Using simple title generation for free plan user"); - return simpleTitleFromMessage; - } - - // For paid plans, use LLM to generate a smart title - try { - console.log("Using AI title generation for paid plan user"); - // Get the user's first message, truncate if too long - const userContent = messageText.slice(0, 500); // Reduced to 500 chars to optimize token usage - - // Use the OpenAI API to generate a concise title - use the default model - const stream = await openai.chat.completions.create({ - model: DEFAULT_MODEL_ID, // Use the default model instead of user selected model - messages: [ - { - role: "system", - content: - "You are a helpful assistant that generates concise, meaningful titles (3-5 words) for chat conversations based on the user's first message. Return only the title without quotes or explanations." - }, - { - role: "user", - content: `Generate a concise, contextual title (3-5 words) for a chat that starts with this message: "${userContent}"` - } - ], - temperature: 0.7, - max_tokens: 15, // Keep response very short - stream: true - } as any); - - let generatedTitle = ""; - for await (const chunk of stream as any) { - const content = chunk.choices?.[0]?.delta?.content || ""; - if (content) generatedTitle += content; - } - - // Remove quotes if present and limit length - const cleanTitle = generatedTitle - .replace(/^["']|["']$/g, "") // Remove leading/trailing quotes - .trim() - .slice(0, 50); - - console.log("Generated title:", cleanTitle); - return cleanTitle || simpleTitleFromMessage; // Fallback if generation fails - } catch (error) { - console.error("Error generating AI title, falling back to simple title:", error); - return simpleTitleFromMessage; - } -} diff --git a/frontend/src/routes/_auth.chat.$chatId.tsx b/frontend/src/routes/_auth.chat.$chatId.tsx index 582bb11f0..671db2a06 100644 --- a/frontend/src/routes/_auth.chat.$chatId.tsx +++ b/frontend/src/routes/_auth.chat.$chatId.tsx @@ -201,6 +201,10 @@ function ChatComponent() { setImageConversionError(`${failedCount} image(s) failed to process. Please try again.`); // Clear error after 5 seconds setTimeout(() => setImageConversionError(null), 5000); + }, + onThreadCreated: (threadId) => { + // Update URL to the thread ID without causing a navigation + navigate(`/chat/${threadId}`, { replace: true }); } }); @@ -438,7 +442,7 @@ END OF INSTRUCTIONS`; // 2. Stream the summary let summary = ""; - const stream = openai.beta.chat.completions.stream({ + const stream = await openai.chat.completions.create({ model: DEFAULT_MODEL_ID, // Use the default model instead of user selected model messages: summarizationMessages, temperature: 0.3, @@ -446,10 +450,9 @@ END OF INSTRUCTIONS`; stream: true }); - for await (const chunk of stream) { + for await (const chunk of stream as any) { summary += chunk.choices[0]?.delta?.content ?? ""; } - await stream.finalChatCompletion(); // 3. Build initial message for the new chat const initialMsg = diff --git a/frontend/src/routes/index.tsx b/frontend/src/routes/index.tsx index 631ee0801..2c12bca63 100644 --- a/frontend/src/routes/index.tsx +++ b/frontend/src/routes/index.tsx @@ -150,8 +150,9 @@ function Index() { // TODO: Add document metadata to LocalState } - const id = await localState.addChat(); - navigate({ to: "/chat/$chatId", params: { chatId: id } }); + // With Responses API, we don't pre-create chats. Navigate to a "new" chat + // and let the chat route handle creating the response when the message is sent + navigate({ to: "/chat/$chatId", params: { chatId: "new" } }); } return ( diff --git a/frontend/src/state/LocalStateContext.tsx b/frontend/src/state/LocalStateContext.tsx index 9d92bacdf..b98d58989 100644 --- a/frontend/src/state/LocalStateContext.tsx +++ b/frontend/src/state/LocalStateContext.tsx @@ -48,6 +48,22 @@ export const LocalStateProvider = ({ children }: { children: React.ReactNode }) const { get, put, list, del } = useOpenSecret(); async function persistChat(chat: Chat) { + // Check if this is a Responses API chat by checking if it exists in the responses list + // If it is, skip local persistence as it's managed server-side + try { + const { fetchResponsesList } = await import("@opensecret/react"); + const responsesData = await fetchResponsesList({ limit: 100 }); + const isResponsesChat = responsesData?.data?.some((r: any) => r.id === chat.id); + + if (isResponsesChat) { + console.log(`Skipping local persistence for Responses API chat: ${chat.id}`); + return; // Don't persist Responses API chats locally + } + } catch (error) { + // If we can't check, continue with local persistence + console.error("Error checking if chat is from Responses API:", error); + } + const chatToSave = { /** If a model is missing, assume the default Llama and write it now */ model: aliasModelName(chat.model) || DEFAULT_MODEL_ID, @@ -130,22 +146,94 @@ export const LocalStateProvider = ({ children }: { children: React.ReactNode }) } async function getChatById(id: string): Promise { + // Special case: "new" means we're creating a new chat + if (id === "new") { + return undefined; // Return undefined for new chats + } + + // First, try to fetch from local storage (legacy chats) try { const chat = await get(`chat_${id}`); - if (!chat) return undefined; - const parsedChat = JSON.parse(chat) as Chat; - // Alias the model name for backward compatibility - if (parsedChat.model) { - parsedChat.model = aliasModelName(parsedChat.model); + if (chat) { + const parsedChat = JSON.parse(chat) as Chat; + // Alias the model name for backward compatibility + if (parsedChat.model) { + parsedChat.model = aliasModelName(parsedChat.model); + } + return parsedChat; } - return parsedChat; } catch (error) { - console.error("Error fetching chat:", error); - return undefined; + console.error("Error fetching chat from local storage:", error); } + + // If not found locally, create a placeholder for Responses API chats + // This is a temporary workaround. The Responses API tracks conversations server-side + // but doesn't yet provide an endpoint to fetch the full thread history. + // For now, we'll return a placeholder that allows continuing the conversation. + + try { + // Check if this ID exists in the responses list (indicating it's a Responses API chat) + const { fetchResponsesList } = await import("@opensecret/react"); + const responsesData = await fetchResponsesList({ limit: 100 }); + const responseChat = responsesData?.data?.find((r: any) => r.id === id); + + if (responseChat) { + // Try to load locally stored messages for display + // Note: The server maintains the real context, this is just for UI + let messages: ChatMessage[] = []; + try { + const stored = localStorage.getItem(`responses_chat_${id}`); + if (stored) { + const parsed = JSON.parse(stored); + messages = parsed.messages || []; + console.log(`Loaded ${messages.length} messages from local storage for chat ${id}`); + } + } catch (error) { + console.error("Failed to load local messages:", error); + } + + console.log(`Found Responses API chat ${id}, returning with ${messages.length} local messages`); + + return { + id: responseChat.id, + title: responseChat.title || "Chat", + messages, // Use locally stored messages for display + model: aliasModelName(responseChat.model) || DEFAULT_MODEL_ID, + // Flag to indicate this is a Responses API chat (for UI purposes) + isResponsesChat: true + } as Chat & { isResponsesChat?: boolean }; + } + } catch (error) { + console.error("Error checking responses list:", error); + } + + return undefined; } async function fetchOrCreateHistoryList() { + const allChats: HistoryItem[] = []; + + // 1. First, fetch chats from the Responses API + try { + const { fetchResponsesList } = await import("@opensecret/react"); + const responsesData = await fetchResponsesList({ limit: 50 }); + console.log("Responses API data:", responsesData); + if (responsesData && responsesData.data) { + // Convert Responses API format to HistoryItem format + const responsesChats = responsesData.data.map((response) => ({ + id: response.id, + title: response.title || "New Chat", // Title should be included in list response + created_at: response.created_at * 1000, // Convert to milliseconds + updated_at: response.created_at * 1000, // Use created_at since updated_at not in response + isResponsesAPI: true // Flag to identify these are from Responses API + })); + allChats.push(...responsesChats); + } + } catch (error) { + console.error("Error fetching responses list:", error); + } + + // 2. Then, fetch legacy chats from local storage let historyList = "[]"; try { const existingHistory = await get("history_list"); @@ -157,7 +245,7 @@ export const LocalStateProvider = ({ children }: { children: React.ReactNode }) } // Parse the history_list item - let parsedHistory: HistoryItem[]; + let parsedHistory: HistoryItem[] = []; try { parsedHistory = JSON.parse(historyList) as HistoryItem[]; if (!Array.isArray(parsedHistory)) { @@ -191,14 +279,31 @@ export const LocalStateProvider = ({ children }: { children: React.ReactNode }) newHistoryList.sort((a, b) => b.updated_at - a.updated_at); await put("history_list", JSON.stringify(newHistoryList)); - return newHistoryList; + parsedHistory = newHistoryList; } catch (error) { console.error("Error creating new history list:", error); - return []; + parsedHistory = []; } } - return parsedHistory; + // Combine both sources, deduplicate by ID (Responses API takes precedence) + const chatMap = new Map(); + + // Add legacy chats first + parsedHistory.forEach(chat => { + chatMap.set(chat.id, chat); + }); + + // Add Responses API chats (will override any duplicates from legacy) + allChats.forEach(chat => { + chatMap.set(chat.id, chat); + }); + + // Convert back to array and sort by updated_at + const dedupedChats = Array.from(chatMap.values()); + dedupedChats.sort((a, b) => (b.updated_at || 0) - (a.updated_at || 0)); + + return dedupedChats; } async function clearHistory() { diff --git a/frontend/src/state/LocalStateContextDef.ts b/frontend/src/state/LocalStateContextDef.ts index 19abef5bf..02b51d22e 100644 --- a/frontend/src/state/LocalStateContextDef.ts +++ b/frontend/src/state/LocalStateContextDef.ts @@ -34,6 +34,7 @@ export type HistoryItem = { title: string; updated_at: number; created_at: number; + isResponsesAPI?: boolean; // True if this chat is from the Responses API }; export type LocalState = { From c477058c16c608cd90b088fc83b3a8552b41911e Mon Sep 17 00:00:00 2001 From: Paul Miller Date: Wed, 17 Sep 2025 09:14:54 -0500 Subject: [PATCH 4/5] Responses: create conversation on first send and send with conversation param; remove previous_response_id; leave URL unchanged (TODO). Add basic error TODOs and query guard for new chats. --- frontend/src/hooks/useChatSession.ts | 50 ++++++++++++---------------- 1 file changed, 22 insertions(+), 28 deletions(-) diff --git a/frontend/src/hooks/useChatSession.ts b/frontend/src/hooks/useChatSession.ts index 905752ac8..a7459fc57 100644 --- a/frontend/src/hooks/useChatSession.ts +++ b/frontend/src/hooks/useChatSession.ts @@ -37,6 +37,7 @@ export function useChatSession( const { data: serverChat, isPending } = useQuery({ queryKey: ["chat", chatId], queryFn: () => getChatById(chatId), + enabled: chatId !== "new", retry: false }); @@ -191,6 +192,8 @@ export function useChatSession( }); try { + // TODO: If we detect legacy KV chats in getChatById, block sends here and show a read-only banner. + // For this user there are no legacy chats to test; leaving as a future enhancement. // const useResponses = import.meta.env.VITE_USE_RESPONSES === "true"; const useResponses = true; @@ -207,34 +210,35 @@ export function useChatSession( let stream: any; try { - // Use the thread ID if we have it (for follow-up messages in the same session) - // or use the chatId if it's a valid UUID (for loading existing threads) - const effectiveThreadId = threadIdRef.current || + // Determine or create the conversation ID + let conversationId = threadIdRef.current || (chatId !== "new" && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(chatId) ? chatId : null); - + + if (!conversationId) { + // Create a new conversation on first send + const conv = await openai.conversations.create({} as any); + conversationId = (conv as any).id as string; + threadIdRef.current = conversationId; + // Do not update URL; requirement is just to get conversation id and send within it. + } + const requestParams: any = { model, input: inputText, instructions, stream: true, - store: true // Ensure messages are persisted + store: true, + conversation: conversationId }; - - // If we have a thread ID, this is a continuation - if (effectiveThreadId) { - requestParams.previous_response_id = effectiveThreadId; - console.log("Continuing thread with previous_response_id:", effectiveThreadId); - } else { - console.log("Starting new thread (no previous_response_id)"); - } - - console.log("Creating response with params:", requestParams); + + console.log("Creating response with params:", { ...requestParams, input: "" }); stream = await openai.responses.create(requestParams); } catch (error) { console.error("Error creating stream:", error); if (error instanceof Error) { console.error("Error details:", error.message, error.stack); } + // TODO: Handle 500/429 errors more gracefully (rate limit backoff, retry-after, etc.) setStreamingError("Failed to create response stream"); setPhase("idle"); processingRef.current = false; @@ -249,19 +253,9 @@ export function useChatSession( // Handle different event types based on OpenAI Responses API spec if (event.type === "response.created") { - // Capture the response ID + // Response/job created (id is response id, not conversation) responseId = event.response?.id; console.log("Response created with ID:", responseId); - - // For new chats, store the thread ID for follow-up messages - if (chatId === "new" && !threadIdRef.current && responseId) { - console.log("New thread created with ID:", responseId); - threadIdRef.current = responseId; - // Call the callback to update the URL via React Router - if (onThreadCreated) { - onThreadCreated(responseId); - } - } } else if (event.type === "response.output_item.added") { // New output item started } else if (event.type === "response.output_text.delta") { @@ -282,8 +276,8 @@ export function useChatSession( setCurrentStreamingMessage(undefined); setPhase("idle"); - // Store messages locally for display purposes (Responses API maintains real context) - // This is a temporary solution until we have a thread history endpoint + // TODO: Remove localStorage mirror once Conversations items are fully used for history + // Store messages locally for display only (server maintains real context) if (threadIdRef.current) { try { localStorage.setItem( From 4acffcf67bf1007a0d44750c3eff536e08922359 Mon Sep 17 00:00:00 2001 From: Paul Miller Date: Sun, 21 Sep 2025 03:48:19 -0500 Subject: [PATCH 5/5] pushing what I have wip --- NEW_UPDATE_TO_CONVERSATIONS_PLAN.md | 4 +- frontend/src/hooks/useChatSession.ts | 131 ++++++++++++++++++--- frontend/src/routes/_auth.chat.$chatId.tsx | 15 ++- frontend/src/state/LocalStateContext.tsx | 123 +++++++------------ frontend/vite.config.ts | 3 + 5 files changed, 173 insertions(+), 103 deletions(-) diff --git a/NEW_UPDATE_TO_CONVERSATIONS_PLAN.md b/NEW_UPDATE_TO_CONVERSATIONS_PLAN.md index f3235ce13..ca626c687 100644 --- a/NEW_UPDATE_TO_CONVERSATIONS_PLAN.md +++ b/NEW_UPDATE_TO_CONVERSATIONS_PLAN.md @@ -145,8 +145,8 @@ References: ## Implementation Checklist API-correctness first -- [ ] Replace `previous_response_id` with `conversation` param in all sends -- [ ] Create conversation before first send; update URL via `replaceState` +- [x] Replace `previous_response_id` with `conversation` param in all sends +- [x] Create conversation on first send; update URL via `history.replaceState` (no navigation) - [ ] Ensure Responses stream consumption handles event types per backend Conversation list and loading diff --git a/frontend/src/hooks/useChatSession.ts b/frontend/src/hooks/useChatSession.ts index a7459fc57..14e5f5fd4 100644 --- a/frontend/src/hooks/useChatSession.ts +++ b/frontend/src/hooks/useChatSession.ts @@ -15,6 +15,7 @@ interface UseChatSessionOptions { openai: ReturnType; model: string; onThreadCreated?: (threadId: string) => void; // Callback when a new thread is created + onThreadFinalized?: (threadId: string) => void; // Callback when stream completes } export function useChatSession( @@ -23,7 +24,7 @@ export function useChatSession( onImageConversionError?: (failedCount: number) => void; } ) { - const { getChatById, persistChat, openai, model, onImageConversionError, onThreadCreated } = options; + const { getChatById, persistChat, openai, model, onImageConversionError, onThreadCreated, onThreadFinalized } = options; const queryClient = useQueryClient(); const [phase, setPhase] = useState("idle"); const [optimisticChat, setOptimisticChat] = useState(null); @@ -41,30 +42,61 @@ export function useChatSession( retry: false }); - // Reset optimistic chat when chatId changes + // Reset optimistic chat when chatId changes, but do NOT abort if the change + // is the initial transition from "new" to the newly created conversation id. + const previousChatIdRef = useRef(chatId); useEffect(() => { - // Abort any ongoing streaming when chatId changes - if (abortControllerRef.current) { - abortControllerRef.current.abort(); - abortControllerRef.current = null; + const prev = previousChatIdRef.current; + const isConversationId = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(chatId); + const initialNewToConversation = prev === "new" && isConversationId && threadIdRef.current === chatId; + + if (!initialNewToConversation) { + // Abort any ongoing streaming when truly navigating to a different chat + if (abortControllerRef.current) { + abortControllerRef.current.abort(); + abortControllerRef.current = null; + } + + setOptimisticChat(null); + setPhase("idle"); + setCurrentStreamingMessage(undefined); + setStreamingError(null); + processingRef.current = false; } - setOptimisticChat(null); - setPhase("idle"); - setCurrentStreamingMessage(undefined); - setStreamingError(null); - processingRef.current = false; - - // Reset thread ID when navigating to a different chat - // But keep it if we're loading an existing thread + // Update thread ref for existing conversations if (chatId === "new") { - threadIdRef.current = null; - } else if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(chatId)) { - // This is an existing thread ID + if (!initialNewToConversation) threadIdRef.current = null; + } else if (isConversationId) { threadIdRef.current = chatId; } + + previousChatIdRef.current = chatId; }, [chatId]); + // Load just the conversation title for the current conversation id + useEffect(() => { + const loadTitle = async () => { + try { + if (!openai) return; + const cid = threadIdRef.current || (chatId !== "new" && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(chatId) ? chatId : null); + if (!cid) return; + const conv = await openai.conversations.retrieve(cid); + const title = (conv as any)?.metadata?.title; + if (title && typeof title === "string") { + setOptimisticChat((prev) => { + if (prev) { + if (prev.title === title) return prev; + return { ...prev, title }; + } + return { id: cid, title, messages: [] } as Chat; + }); + } + } catch {} + }; + loadTitle(); + }, [chatId, openai]); + // Cleanup on unmount useEffect(() => { return () => { @@ -74,6 +106,18 @@ export function useChatSession( }; }, []); + // Allow using a search param to pin the conversation id while streaming on /chat/new + useEffect(() => { + if (chatId !== "new" || threadIdRef.current) return; + try { + const usp = new URLSearchParams(window.location.search); + const cid = usp.get("conversation_id"); + if (cid && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(cid)) { + threadIdRef.current = cid; + } + } catch {} + }, [chatId]); + // Part A: Apply guard when syncing server data to optimistic state useEffect(() => { if (!serverChat || isPending) return; @@ -86,6 +130,51 @@ export function useChatSession( }); }, [serverChat, isPending]); + // Load conversation items for existing conversations so hard refresh shows history + useEffect(() => { + const loadConversationItems = async () => { + try { + if (!openai) return; + if (!chatId || chatId === "new") return; + if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(chatId)) return; + + // Avoid re-loading if we already have messages + const current = optimisticChat || serverChat; + if (current && current.messages && current.messages.length > 0) return; + + threadIdRef.current = chatId; + const items = await openai.conversations.items.list(chatId, { order: "asc", limit: 100 }); + + // Some backends may ignore the 'order' param. Sort explicitly by created_at ascending. + const messageItems = (items.data || []) + .filter((it: any) => it.type === "message" && (it.role === "user" || it.role === "assistant")) + .map((it: any) => { + let text = ""; + if (Array.isArray(it.content)) { + for (const part of it.content) { + if (part?.type === "text" && typeof part.text === "string") text += part.text; + } + } else if (typeof it.content === "string") { + text = it.content; + } + return { role: it.role, content: text, created_at: it.created_at ?? 0 } as any; + }) + .sort((a: any, b: any) => (a.created_at || 0) - (b.created_at || 0)); + + const messages: ChatMessage[] = messageItems.map(({ role, content }) => ({ role, content })); + + setOptimisticChat({ id: chatId, title: "New Conversation", messages }); + } catch (err) { + console.error("Failed to load conversation items:", err); + } + }; + + // Only load when there is no server chat (KV) and we're on a conversation id + if (!serverChat && chatId !== "new") { + loadConversationItems(); + } + }, [chatId, openai, optimisticChat, serverChat]); + // Mutation for persisting chat const persistMutation = useMutation({ mutationFn: persistChat, @@ -219,7 +308,13 @@ export function useChatSession( const conv = await openai.conversations.create({} as any); conversationId = (conv as any).id as string; threadIdRef.current = conversationId; - // Do not update URL; requirement is just to get conversation id and send within it. + // Inform caller so they can update the URL without navigating + if (onThreadCreated) { + try { onThreadCreated(conversationId); } catch {} + } else { + // Fallback: update URL directly, no navigation + try { window.history.replaceState(null, "", `/chat/${conversationId}`); } catch {} + } } const requestParams: any = { diff --git a/frontend/src/routes/_auth.chat.$chatId.tsx b/frontend/src/routes/_auth.chat.$chatId.tsx index 671db2a06..688cac87a 100644 --- a/frontend/src/routes/_auth.chat.$chatId.tsx +++ b/frontend/src/routes/_auth.chat.$chatId.tsx @@ -203,7 +203,17 @@ function ChatComponent() { setTimeout(() => setImageConversionError(null), 5000); }, onThreadCreated: (threadId) => { - // Update URL to the thread ID without causing a navigation + // Update only the search param during streaming to avoid remounting + try { + const usp = new URLSearchParams(window.location.search); + usp.set("conversation_id", threadId); + window.history.replaceState(null, "", `${window.location.pathname}?${usp.toString()}`); + } catch (e) { + console.warn("Failed to set conversation_id in URL", e); + } + }, + onThreadFinalized: (threadId) => { + // Promote to canonical path after streaming completes navigate(`/chat/${threadId}`, { replace: true }); } }); @@ -591,7 +601,8 @@ END OF INSTRUCTIONS`; () }); - const { get, put, list, del } = useOpenSecret(); + const { get, put, list, del, listConversations } = useOpenSecret() as any; async function persistChat(chat: Chat) { - // Check if this is a Responses API chat by checking if it exists in the responses list - // If it is, skip local persistence as it's managed server-side - try { - const { fetchResponsesList } = await import("@opensecret/react"); - const responsesData = await fetchResponsesList({ limit: 100 }); - const isResponsesChat = responsesData?.data?.some((r: any) => r.id === chat.id); - - if (isResponsesChat) { - console.log(`Skipping local persistence for Responses API chat: ${chat.id}`); - return; // Don't persist Responses API chats locally - } - } catch (error) { - // If we can't check, continue with local persistence - console.error("Error checking if chat is from Responses API:", error); + // Skip local persistence for server-managed Conversations + const convs = await listConversations({ limit: 100 }); + const isConversation = convs?.data?.some((c: any) => c.id === chat.id); + if (isConversation) { + console.log(`Skipping local persistence for Conversation chat: ${chat.id}`); + return; } const chatToSave = { @@ -148,89 +140,58 @@ export const LocalStateProvider = ({ children }: { children: React.ReactNode }) async function getChatById(id: string): Promise { // Special case: "new" means we're creating a new chat if (id === "new") { - return undefined; // Return undefined for new chats + return undefined; } - - // First, try to fetch from local storage (legacy chats) + + // Prefer Conversations: if this id is a conversation, return a placeholder + try { + const convs = await listConversations({ limit: 100 }); + const conv = convs?.data?.find((c: any) => c.id === id); + if (conv) { + return { + id: conv.id, + title: conv.metadata?.title || "New Conversation", + messages: [], + model: DEFAULT_MODEL_ID, + isResponsesChat: true + } as any; + } + } catch (error) { + console.error("Error checking conversations list:", error); + } + + // Fallback to legacy KV storage try { const chat = await get(`chat_${id}`); if (chat) { const parsedChat = JSON.parse(chat) as Chat; - // Alias the model name for backward compatibility if (parsedChat.model) { parsedChat.model = aliasModelName(parsedChat.model); } + (parsedChat as any).isLegacy = true; return parsedChat; } - } catch (error) { - console.error("Error fetching chat from local storage:", error); + } catch (_error) { + // Ignore missing KV entries } - // If not found locally, create a placeholder for Responses API chats - // This is a temporary workaround. The Responses API tracks conversations server-side - // but doesn't yet provide an endpoint to fetch the full thread history. - // For now, we'll return a placeholder that allows continuing the conversation. - - try { - // Check if this ID exists in the responses list (indicating it's a Responses API chat) - const { fetchResponsesList } = await import("@opensecret/react"); - const responsesData = await fetchResponsesList({ limit: 100 }); - const responseChat = responsesData?.data?.find((r: any) => r.id === id); - - if (responseChat) { - // Try to load locally stored messages for display - // Note: The server maintains the real context, this is just for UI - let messages: ChatMessage[] = []; - try { - const stored = localStorage.getItem(`responses_chat_${id}`); - if (stored) { - const parsed = JSON.parse(stored); - messages = parsed.messages || []; - console.log(`Loaded ${messages.length} messages from local storage for chat ${id}`); - } - } catch (error) { - console.error("Failed to load local messages:", error); - } - - console.log(`Found Responses API chat ${id}, returning with ${messages.length} local messages`); - - return { - id: responseChat.id, - title: responseChat.title || "Chat", - messages, // Use locally stored messages for display - model: aliasModelName(responseChat.model) || DEFAULT_MODEL_ID, - // Flag to indicate this is a Responses API chat (for UI purposes) - isResponsesChat: true - } as Chat & { isResponsesChat?: boolean }; - } - } catch (error) { - console.error("Error checking responses list:", error); - } - return undefined; } async function fetchOrCreateHistoryList() { const allChats: HistoryItem[] = []; - // 1. First, fetch chats from the Responses API - try { - const { fetchResponsesList } = await import("@opensecret/react"); - const responsesData = await fetchResponsesList({ limit: 50 }); - console.log("Responses API data:", responsesData); - if (responsesData && responsesData.data) { - // Convert Responses API format to HistoryItem format - const responsesChats = responsesData.data.map((response) => ({ - id: response.id, - title: response.title || "New Chat", // Title should be included in list response - created_at: response.created_at * 1000, // Convert to milliseconds - updated_at: response.created_at * 1000, // Use created_at since updated_at not in response - isResponsesAPI: true // Flag to identify these are from Responses API - })); - allChats.push(...responsesChats); - } - } catch (error) { - console.error("Error fetching responses list:", error); + // 1. First, fetch conversations list (server-managed chats) + const conversationsData = await listConversations({ limit: 50 }); + if (conversationsData && conversationsData.data) { + const convChats = conversationsData.data.map((conv: any) => ({ + id: conv.id, + title: conv.metadata?.title || "New Conversation", + created_at: conv.created_at * 1000, + updated_at: conv.created_at * 1000, + isResponsesAPI: true + })); + allChats.push(...convChats); } // 2. Then, fetch legacy chats from local storage @@ -286,7 +247,7 @@ export const LocalStateProvider = ({ children }: { children: React.ReactNode }) } } - // Combine both sources, deduplicate by ID (Responses API takes precedence) + // Combine both sources, deduplicate by ID (Conversations take precedence) const chatMap = new Map(); // Add legacy chats first diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 17a924c3c..1dc52f511 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -7,6 +7,9 @@ import derPlugin from "./vite-der-plugin"; // https://vitejs.dev/config/ export default defineConfig({ plugins: [TanStackRouterVite(), react(), derPlugin()], + server: { + host: "127.0.0.1" + }, resolve: { alias: { "@": path.resolve(__dirname, "./src")