@aphexcms/cms-core 9.7.0 → 9.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-chat-state.svelte.d.ts +38 -0
- package/dist/agent-chat-state.svelte.d.ts.map +1 -0
- package/dist/agent-chat-state.svelte.js +86 -0
- package/dist/ai/content-workspace-tools.d.ts +3 -0
- package/dist/ai/content-workspace-tools.d.ts.map +1 -0
- package/dist/ai/content-workspace-tools.js +50 -0
- package/dist/ai/default-system-prompt.d.ts +2 -0
- package/dist/ai/default-system-prompt.d.ts.map +1 -0
- package/dist/ai/default-system-prompt.js +15 -0
- package/dist/ai/index.d.ts +2 -0
- package/dist/ai/index.d.ts.map +1 -0
- package/dist/ai/index.js +4 -0
- package/dist/ai/interfaces/ai-provider.d.ts +67 -0
- package/dist/ai/interfaces/ai-provider.d.ts.map +1 -0
- package/dist/ai/interfaces/ai-provider.js +16 -0
- package/dist/ai/run-agent-turn.d.ts +29 -0
- package/dist/ai/run-agent-turn.d.ts.map +1 -0
- package/dist/ai/run-agent-turn.js +166 -0
- package/dist/api/agent-chat.d.ts +27 -0
- package/dist/api/agent-chat.d.ts.map +1 -0
- package/dist/api/agent-chat.js +64 -0
- package/dist/api/documents.d.ts +8 -3
- package/dist/api/documents.d.ts.map +1 -1
- package/dist/api/documents.js +6 -6
- package/dist/api/schemas/agent-chat.d.ts +45 -0
- package/dist/api/schemas/agent-chat.d.ts.map +1 -0
- package/dist/api/schemas/agent-chat.js +35 -0
- package/dist/api/schemas/agent-operations.d.ts +13 -0
- package/dist/api/schemas/agent-operations.d.ts.map +1 -0
- package/dist/api/schemas/agent-operations.js +15 -0
- package/dist/api/schemas/documents.d.ts +22 -0
- package/dist/api/schemas/documents.d.ts.map +1 -1
- package/dist/api/schemas/documents.js +20 -2
- package/dist/chunk-recovery.d.ts +33 -0
- package/dist/chunk-recovery.d.ts.map +1 -0
- package/dist/chunk-recovery.js +131 -0
- package/dist/client/index.d.ts +1 -0
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +1 -0
- package/dist/client/ui.d.ts +1 -0
- package/dist/client/ui.d.ts.map +1 -1
- package/dist/client/ui.js +1 -0
- package/dist/components/AdminApp.svelte +37 -3
- package/dist/components/AdminApp.svelte.d.ts.map +1 -1
- package/dist/components/admin/ActivityView.svelte +251 -42
- package/dist/components/admin/ActivityView.svelte.d.ts.map +1 -1
- package/dist/components/admin/AgentChat.svelte +977 -0
- package/dist/components/admin/AgentChat.svelte.d.ts +17 -0
- package/dist/components/admin/AgentChat.svelte.d.ts.map +1 -0
- package/dist/components/admin/DocumentEditor.svelte +273 -60
- package/dist/components/admin/DocumentEditor.svelte.d.ts +6 -0
- package/dist/components/admin/DocumentEditor.svelte.d.ts.map +1 -1
- package/dist/components/admin/ObjectModal.svelte +4 -2
- package/dist/components/admin/fields/ArrayField.svelte +4 -1
- package/dist/components/admin/fields/ReferenceField.svelte +12 -1
- package/dist/components/admin/fields/ReferenceField.svelte.d.ts.map +1 -1
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +1 -0
- package/dist/components/layout/Sidebar.svelte +26 -2
- package/dist/components/layout/Sidebar.svelte.d.ts +1 -0
- package/dist/components/layout/Sidebar.svelte.d.ts.map +1 -1
- package/dist/db/interfaces/agent-change-sets.d.ts +17 -0
- package/dist/db/interfaces/agent-change-sets.d.ts.map +1 -0
- package/dist/db/interfaces/agent-change-sets.js +1 -0
- package/dist/db/interfaces/document.d.ts +25 -3
- package/dist/db/interfaces/document.d.ts.map +1 -1
- package/dist/db/interfaces/document.js +19 -1
- package/dist/db/interfaces/index.d.ts +4 -1
- package/dist/db/interfaces/index.d.ts.map +1 -1
- package/dist/db/interfaces/index.js +1 -1
- package/dist/document-refresh.svelte.d.ts +2 -0
- package/dist/document-refresh.svelte.d.ts.map +1 -1
- package/dist/document-refresh.svelte.js +15 -0
- package/dist/document-workspace-registry.svelte.d.ts +15 -0
- package/dist/document-workspace-registry.svelte.d.ts.map +1 -0
- package/dist/document-workspace-registry.svelte.js +30 -0
- package/dist/field-validation/utils.d.ts.map +1 -1
- package/dist/field-validation/utils.js +211 -29
- package/dist/hooks.d.ts +2 -0
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +2 -0
- package/dist/lib/agent-chat-state.svelte.d.ts +38 -0
- package/dist/lib/agent-chat-state.svelte.d.ts.map +1 -0
- package/dist/lib/agent-chat-state.svelte.js +87 -0
- package/dist/lib/agent-chat-state.svelte.js.map +1 -0
- package/dist/lib/ai/content-workspace-tools.d.ts +3 -0
- package/dist/lib/ai/content-workspace-tools.d.ts.map +1 -0
- package/dist/lib/ai/content-workspace-tools.js +51 -0
- package/dist/lib/ai/content-workspace-tools.js.map +1 -0
- package/dist/lib/ai/default-system-prompt.d.ts +2 -0
- package/dist/lib/ai/default-system-prompt.d.ts.map +1 -0
- package/dist/lib/ai/default-system-prompt.js +16 -0
- package/dist/lib/ai/default-system-prompt.js.map +1 -0
- package/dist/lib/ai/index.d.ts +2 -0
- package/dist/lib/ai/index.d.ts.map +1 -0
- package/dist/lib/ai/index.js +5 -0
- package/dist/lib/ai/index.js.map +1 -0
- package/dist/lib/ai/interfaces/ai-provider.d.ts +67 -0
- package/dist/lib/ai/interfaces/ai-provider.d.ts.map +1 -0
- package/dist/lib/ai/interfaces/ai-provider.js +17 -0
- package/dist/lib/ai/interfaces/ai-provider.js.map +1 -0
- package/dist/lib/ai/run-agent-turn.d.ts +29 -0
- package/dist/lib/ai/run-agent-turn.d.ts.map +1 -0
- package/dist/lib/ai/run-agent-turn.js +167 -0
- package/dist/lib/ai/run-agent-turn.js.map +1 -0
- package/dist/lib/api/agent-chat.d.ts +27 -0
- package/dist/lib/api/agent-chat.d.ts.map +1 -0
- package/dist/lib/api/agent-chat.js +65 -0
- package/dist/lib/api/agent-chat.js.map +1 -0
- package/dist/lib/api/documents.d.ts +8 -3
- package/dist/lib/api/documents.d.ts.map +1 -1
- package/dist/lib/api/documents.js +6 -6
- package/dist/lib/api/documents.js.map +1 -1
- package/dist/lib/api/schemas/agent-chat.d.ts +45 -0
- package/dist/lib/api/schemas/agent-chat.d.ts.map +1 -0
- package/dist/lib/api/schemas/agent-chat.js +36 -0
- package/dist/lib/api/schemas/agent-chat.js.map +1 -0
- package/dist/lib/api/schemas/agent-operations.d.ts +13 -0
- package/dist/lib/api/schemas/agent-operations.d.ts.map +1 -0
- package/dist/lib/api/schemas/agent-operations.js +16 -0
- package/dist/lib/api/schemas/agent-operations.js.map +1 -0
- package/dist/lib/api/schemas/documents.d.ts +22 -0
- package/dist/lib/api/schemas/documents.d.ts.map +1 -1
- package/dist/lib/api/schemas/documents.js +20 -2
- package/dist/lib/api/schemas/documents.js.map +1 -1
- package/dist/lib/chunk-recovery.d.ts +33 -0
- package/dist/lib/chunk-recovery.d.ts.map +1 -0
- package/dist/lib/chunk-recovery.js +132 -0
- package/dist/lib/chunk-recovery.js.map +1 -0
- package/dist/lib/client/index.d.ts +1 -0
- package/dist/lib/client/index.d.ts.map +1 -1
- package/dist/lib/client/index.js +1 -0
- package/dist/lib/client/index.js.map +1 -1
- package/dist/lib/client/ui.d.ts +1 -0
- package/dist/lib/client/ui.d.ts.map +1 -1
- package/dist/lib/client/ui.js +1 -0
- package/dist/lib/client/ui.js.map +1 -1
- package/dist/lib/components/index.d.ts +1 -0
- package/dist/lib/components/index.d.ts.map +1 -1
- package/dist/lib/components/index.js +1 -0
- package/dist/lib/components/index.js.map +1 -1
- package/dist/lib/db/interfaces/agent-change-sets.d.ts +17 -0
- package/dist/lib/db/interfaces/agent-change-sets.d.ts.map +1 -0
- package/dist/lib/db/interfaces/agent-change-sets.js +2 -0
- package/dist/lib/db/interfaces/agent-change-sets.js.map +1 -0
- package/dist/lib/db/interfaces/document.d.ts +25 -3
- package/dist/lib/db/interfaces/document.d.ts.map +1 -1
- package/dist/lib/db/interfaces/document.js +19 -1
- package/dist/lib/db/interfaces/document.js.map +1 -1
- package/dist/lib/db/interfaces/index.d.ts +4 -1
- package/dist/lib/db/interfaces/index.d.ts.map +1 -1
- package/dist/lib/db/interfaces/index.js +1 -1
- package/dist/lib/db/interfaces/index.js.map +1 -1
- package/dist/lib/document-refresh.svelte.d.ts +2 -0
- package/dist/lib/document-refresh.svelte.d.ts.map +1 -1
- package/dist/lib/document-refresh.svelte.js +15 -0
- package/dist/lib/document-refresh.svelte.js.map +1 -1
- package/dist/lib/document-workspace-registry.svelte.d.ts +15 -0
- package/dist/lib/document-workspace-registry.svelte.d.ts.map +1 -0
- package/dist/lib/document-workspace-registry.svelte.js +31 -0
- package/dist/lib/document-workspace-registry.svelte.js.map +1 -0
- package/dist/lib/field-validation/utils.d.ts.map +1 -1
- package/dist/lib/field-validation/utils.js +211 -29
- package/dist/lib/field-validation/utils.js.map +1 -1
- package/dist/lib/hooks.d.ts +2 -0
- package/dist/lib/hooks.d.ts.map +1 -1
- package/dist/lib/hooks.js +2 -0
- package/dist/lib/hooks.js.map +1 -1
- package/dist/lib/local-api/collection-api.d.ts +7 -2
- package/dist/lib/local-api/collection-api.d.ts.map +1 -1
- package/dist/lib/local-api/collection-api.js +17 -10
- package/dist/lib/local-api/collection-api.js.map +1 -1
- package/dist/lib/mcp/tools.d.ts +39 -4
- package/dist/lib/mcp/tools.d.ts.map +1 -1
- package/dist/lib/mcp/tools.js +546 -362
- package/dist/lib/mcp/tools.js.map +1 -1
- package/dist/lib/plugins/resolver.d.ts +8 -1
- package/dist/lib/plugins/resolver.d.ts.map +1 -1
- package/dist/lib/plugins/resolver.js +14 -1
- package/dist/lib/plugins/resolver.js.map +1 -1
- package/dist/lib/plugins/types.d.ts +23 -2
- package/dist/lib/plugins/types.d.ts.map +1 -1
- package/dist/lib/plugins/types.js.map +1 -1
- package/dist/lib/preview/stega.d.ts +2 -2
- package/dist/lib/preview/stega.d.ts.map +1 -1
- package/dist/lib/preview/stega.js +52 -29
- package/dist/lib/preview/stega.js.map +1 -1
- package/dist/lib/schema-utils/validator.d.ts.map +1 -1
- package/dist/lib/schema-utils/validator.js +5 -2
- package/dist/lib/schema-utils/validator.js.map +1 -1
- package/dist/lib/server/api/index.d.ts.map +1 -1
- package/dist/lib/server/api/index.js +8 -0
- package/dist/lib/server/api/index.js.map +1 -1
- package/dist/lib/server/api/routes/agent-change-sets.d.ts +4 -0
- package/dist/lib/server/api/routes/agent-change-sets.d.ts.map +1 -0
- package/dist/lib/server/api/routes/agent-change-sets.js +132 -0
- package/dist/lib/server/api/routes/agent-change-sets.js.map +1 -0
- package/dist/lib/server/api/routes/agent-chat.d.ts +14 -0
- package/dist/lib/server/api/routes/agent-chat.d.ts.map +1 -0
- package/dist/lib/server/api/routes/agent-chat.js +224 -0
- package/dist/lib/server/api/routes/agent-chat.js.map +1 -0
- package/dist/lib/server/api/routes/document-versions.d.ts.map +1 -1
- package/dist/lib/server/api/routes/document-versions.js +16 -2
- package/dist/lib/server/api/routes/document-versions.js.map +1 -1
- package/dist/lib/server/api/routes/documents-by-id.d.ts.map +1 -1
- package/dist/lib/server/api/routes/documents-by-id.js +11 -1
- package/dist/lib/server/api/routes/documents-by-id.js.map +1 -1
- package/dist/lib/server/api/routes/documents-publish.d.ts.map +1 -1
- package/dist/lib/server/api/routes/documents-publish.js +36 -3
- package/dist/lib/server/api/routes/documents-publish.js.map +1 -1
- package/dist/lib/server/api/routes/jobs.d.ts.map +1 -1
- package/dist/lib/server/api/routes/jobs.js +4 -2
- package/dist/lib/server/api/routes/jobs.js.map +1 -1
- package/dist/lib/server/api/routes/resolve-created-by.d.ts +13 -0
- package/dist/lib/server/api/routes/resolve-created-by.d.ts.map +1 -0
- package/dist/lib/server/api/routes/resolve-created-by.js +31 -0
- package/dist/lib/server/api/routes/resolve-created-by.js.map +1 -0
- package/dist/lib/server/index.d.ts +1 -0
- package/dist/lib/server/index.d.ts.map +1 -1
- package/dist/lib/server/index.js +1 -0
- package/dist/lib/server/index.js.map +1 -1
- package/dist/lib/services/references-service.d.ts +1 -1
- package/dist/lib/services/references-service.d.ts.map +1 -1
- package/dist/lib/services/version-service.d.ts +4 -4
- package/dist/lib/services/version-service.d.ts.map +1 -1
- package/dist/lib/services/version-service.js +15 -11
- package/dist/lib/services/version-service.js.map +1 -1
- package/dist/lib/type-gen.d.ts.map +1 -1
- package/dist/lib/type-gen.js +11 -0
- package/dist/lib/type-gen.js.map +1 -1
- package/dist/lib/types/agent-change-sets.d.ts +70 -0
- package/dist/lib/types/agent-change-sets.d.ts.map +1 -0
- package/dist/lib/types/agent-change-sets.js +7 -0
- package/dist/lib/types/agent-change-sets.js.map +1 -0
- package/dist/lib/types/agent-stream.d.ts +60 -0
- package/dist/lib/types/agent-stream.d.ts.map +1 -0
- package/dist/lib/types/agent-stream.js +12 -0
- package/dist/lib/types/agent-stream.js.map +1 -0
- package/dist/lib/types/agent-tools.d.ts +65 -0
- package/dist/lib/types/agent-tools.d.ts.map +1 -0
- package/dist/lib/types/agent-tools.js +2 -0
- package/dist/lib/types/agent-tools.js.map +1 -0
- package/dist/lib/types/config.d.ts +19 -0
- package/dist/lib/types/config.d.ts.map +1 -1
- package/dist/lib/types/document-workspace.d.ts +53 -0
- package/dist/lib/types/document-workspace.d.ts.map +1 -0
- package/dist/lib/types/document-workspace.js +15 -0
- package/dist/lib/types/document-workspace.js.map +1 -0
- package/dist/lib/types/document.d.ts +5 -0
- package/dist/lib/types/document.d.ts.map +1 -1
- package/dist/lib/types/index.d.ts +2 -0
- package/dist/lib/types/index.d.ts.map +1 -1
- package/dist/lib/types/index.js +2 -0
- package/dist/lib/types/index.js.map +1 -1
- package/dist/lib/types/schemas.js +1 -1
- package/dist/lib/types/schemas.js.map +1 -1
- package/dist/lib/utils/fetch-remote-file.d.ts +11 -0
- package/dist/lib/utils/fetch-remote-file.d.ts.map +1 -0
- package/dist/lib/utils/fetch-remote-file.js +169 -0
- package/dist/lib/utils/fetch-remote-file.js.map +1 -0
- package/dist/local-api/collection-api.d.ts +7 -2
- package/dist/local-api/collection-api.d.ts.map +1 -1
- package/dist/local-api/collection-api.js +17 -10
- package/dist/mcp/tools.d.ts +39 -4
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +546 -362
- package/dist/plugins/resolver.d.ts +8 -1
- package/dist/plugins/resolver.d.ts.map +1 -1
- package/dist/plugins/resolver.js +14 -1
- package/dist/plugins/types.d.ts +23 -2
- package/dist/plugins/types.d.ts.map +1 -1
- package/dist/preview/stega.d.ts +2 -2
- package/dist/preview/stega.d.ts.map +1 -1
- package/dist/preview/stega.js +52 -29
- package/dist/schema-utils/validator.d.ts.map +1 -1
- package/dist/schema-utils/validator.js +5 -2
- package/dist/server/api/index.d.ts.map +1 -1
- package/dist/server/api/index.js +8 -0
- package/dist/server/api/routes/agent-change-sets.d.ts +4 -0
- package/dist/server/api/routes/agent-change-sets.d.ts.map +1 -0
- package/dist/server/api/routes/agent-change-sets.js +131 -0
- package/dist/server/api/routes/agent-chat.d.ts +14 -0
- package/dist/server/api/routes/agent-chat.d.ts.map +1 -0
- package/dist/server/api/routes/agent-chat.js +223 -0
- package/dist/server/api/routes/document-versions.d.ts.map +1 -1
- package/dist/server/api/routes/document-versions.js +16 -2
- package/dist/server/api/routes/documents-by-id.d.ts.map +1 -1
- package/dist/server/api/routes/documents-by-id.js +11 -1
- package/dist/server/api/routes/documents-publish.d.ts.map +1 -1
- package/dist/server/api/routes/documents-publish.js +36 -3
- package/dist/server/api/routes/jobs.d.ts.map +1 -1
- package/dist/server/api/routes/jobs.js +4 -2
- package/dist/server/api/routes/resolve-created-by.d.ts +13 -0
- package/dist/server/api/routes/resolve-created-by.d.ts.map +1 -0
- package/dist/server/api/routes/resolve-created-by.js +30 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +1 -0
- package/dist/services/references-service.d.ts +1 -1
- package/dist/services/references-service.d.ts.map +1 -1
- package/dist/services/version-service.d.ts +4 -4
- package/dist/services/version-service.d.ts.map +1 -1
- package/dist/services/version-service.js +15 -11
- package/dist/type-gen.d.ts.map +1 -1
- package/dist/type-gen.js +11 -0
- package/dist/types/agent-change-sets.d.ts +70 -0
- package/dist/types/agent-change-sets.d.ts.map +1 -0
- package/dist/types/agent-change-sets.js +6 -0
- package/dist/types/agent-stream.d.ts +60 -0
- package/dist/types/agent-stream.d.ts.map +1 -0
- package/dist/types/agent-stream.js +11 -0
- package/dist/types/agent-tools.d.ts +65 -0
- package/dist/types/agent-tools.d.ts.map +1 -0
- package/dist/types/agent-tools.js +1 -0
- package/dist/types/config.d.ts +19 -0
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/document-workspace.d.ts +53 -0
- package/dist/types/document-workspace.d.ts.map +1 -0
- package/dist/types/document-workspace.js +14 -0
- package/dist/types/document.d.ts +5 -0
- package/dist/types/document.d.ts.map +1 -1
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +2 -0
- package/dist/types/schemas.js +1 -1
- package/dist/utils/fetch-remote-file.d.ts +11 -0
- package/dist/utils/fetch-remote-file.d.ts.map +1 -0
- package/dist/utils/fetch-remote-file.js +168 -0
- package/package.json +5 -2
|
@@ -0,0 +1,977 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Button } from '@aphexcms/ui/shadcn/button';
|
|
3
|
+
import { Textarea } from '@aphexcms/ui/shadcn/textarea';
|
|
4
|
+
import {
|
|
5
|
+
MessageScroller,
|
|
6
|
+
MessageScrollerButton,
|
|
7
|
+
MessageScrollerContent,
|
|
8
|
+
MessageScrollerItem,
|
|
9
|
+
MessageScrollerProvider,
|
|
10
|
+
MessageScrollerViewport
|
|
11
|
+
} from '@aphexcms/ui/shadcn/message-scroller';
|
|
12
|
+
import { Message, MessageContent, MessageFooter } from '@aphexcms/ui/shadcn/message';
|
|
13
|
+
import { Bubble, BubbleContent } from '@aphexcms/ui/shadcn/bubble';
|
|
14
|
+
import { Marker, MarkerContent, MarkerIcon } from '@aphexcms/ui/shadcn/marker';
|
|
15
|
+
import {
|
|
16
|
+
Bot,
|
|
17
|
+
Check,
|
|
18
|
+
ChevronDown,
|
|
19
|
+
CircleAlert,
|
|
20
|
+
Copy,
|
|
21
|
+
Database,
|
|
22
|
+
FileCode,
|
|
23
|
+
FilePlus,
|
|
24
|
+
FileText,
|
|
25
|
+
Image,
|
|
26
|
+
ImagePlus,
|
|
27
|
+
Info,
|
|
28
|
+
Layers,
|
|
29
|
+
LoaderCircle,
|
|
30
|
+
Pencil,
|
|
31
|
+
RefreshCw,
|
|
32
|
+
Search,
|
|
33
|
+
Send,
|
|
34
|
+
Settings,
|
|
35
|
+
ShieldCheck,
|
|
36
|
+
Sparkles,
|
|
37
|
+
Square,
|
|
38
|
+
Trash2,
|
|
39
|
+
UploadCloud,
|
|
40
|
+
Wrench
|
|
41
|
+
} from '@lucide/svelte';
|
|
42
|
+
import { page } from '$app/state';
|
|
43
|
+
import type { Component } from 'svelte';
|
|
44
|
+
import { marked } from 'marked';
|
|
45
|
+
import DOMPurify from 'dompurify';
|
|
46
|
+
import {
|
|
47
|
+
agentChatState,
|
|
48
|
+
type HistoryMessage,
|
|
49
|
+
type Turn,
|
|
50
|
+
type ToolCall
|
|
51
|
+
} from '../../agent-chat-state.svelte';
|
|
52
|
+
import { documentWorkspaceRegistry } from '../../document-workspace-registry.svelte';
|
|
53
|
+
import type { DocumentWorkspace } from '../../types/document-workspace';
|
|
54
|
+
import type { AgentStreamEvent } from '../../types/agent-stream';
|
|
55
|
+
import { streamAgentChat, recordWorkspaceOperation } from '../../api/agent-chat';
|
|
56
|
+
import { resolvePreviewTitle } from '../../utils/preview';
|
|
57
|
+
import { notifyCollectionChanged } from '../../document-refresh.svelte';
|
|
58
|
+
|
|
59
|
+
/** A suggestion can be a bare string, or `{ text, icon }` for a leading icon — the plain
|
|
60
|
+
* string form stays the common case so existing callers don't need to change anything. */
|
|
61
|
+
type Suggestion = string | { text: string; icon?: Component };
|
|
62
|
+
|
|
63
|
+
const DEFAULT_SUGGESTIONS: Suggestion[] = [
|
|
64
|
+
'List the collections in this CMS',
|
|
65
|
+
'Summarize the latest published documents',
|
|
66
|
+
'Describe the schema for my pages'
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
let {
|
|
70
|
+
embedded = false,
|
|
71
|
+
title = 'Aphex Assistant',
|
|
72
|
+
subtitle = 'CMS-aware answers and content tools',
|
|
73
|
+
suggestions = DEFAULT_SUGGESTIONS
|
|
74
|
+
}: {
|
|
75
|
+
embedded?: boolean;
|
|
76
|
+
title?: string;
|
|
77
|
+
subtitle?: string;
|
|
78
|
+
suggestions?: Suggestion[];
|
|
79
|
+
} = $props();
|
|
80
|
+
|
|
81
|
+
/** Result shape a workspace-tool handler (below) hands back — the same idea as
|
|
82
|
+
* `AgentToolResult`, kept local so this file doesn't need a server-only type import. */
|
|
83
|
+
type WorkspaceToolHandlerResult = { success: boolean; data?: unknown; error?: string };
|
|
84
|
+
|
|
85
|
+
type WorkspaceToolHandler = (
|
|
86
|
+
args: Record<string, unknown>,
|
|
87
|
+
ws: DocumentWorkspace
|
|
88
|
+
) => Promise<WorkspaceToolHandlerResult>;
|
|
89
|
+
|
|
90
|
+
const patchFieldsHandler: WorkspaceToolHandler = async (args, ws) => {
|
|
91
|
+
const fields = (args.fields as Record<string, unknown>) ?? {};
|
|
92
|
+
ws.apply({ type: 'patchFields', fields });
|
|
93
|
+
const validation = await ws.validate();
|
|
94
|
+
return { success: true, data: { applied: Object.keys(fields), validation } };
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const saveDraftHandler: WorkspaceToolHandler = async (_args, ws) => {
|
|
98
|
+
const snapshot = ws.getSnapshot();
|
|
99
|
+
const result = await ws.flushSave(snapshot.revision ?? undefined);
|
|
100
|
+
if (!result.success) {
|
|
101
|
+
return {
|
|
102
|
+
success: false,
|
|
103
|
+
error: result.conflict
|
|
104
|
+
? 'Revision conflict — reload before continuing.'
|
|
105
|
+
: (result.error ?? 'Save failed')
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
return { success: true, data: { revision: result.revision } };
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
/** Resolves `content_patch_fields`/`content_save_draft` against a live `DocumentWorkspace` —
|
|
112
|
+
* see types/document-workspace.ts and ai/content-workspace-tools.ts for why these two tools
|
|
113
|
+
* can't be executed server-side at all. */
|
|
114
|
+
const WORKSPACE_TOOL_HANDLERS: Record<string, WorkspaceToolHandler> = {
|
|
115
|
+
content_patch_fields: patchFieldsHandler,
|
|
116
|
+
content_save_draft: saveDraftHandler
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
/** Only a flush's outcome is worth an audit row — `content_patch_fields` just buffers an
|
|
120
|
+
* in-memory change with nothing durable to attribute yet (see the plan doc's atomicity
|
|
121
|
+
* design: one save = one operation, not one per patch). */
|
|
122
|
+
const WORKSPACE_TOOLS_RECORDED = new Set(['content_save_draft']);
|
|
123
|
+
|
|
124
|
+
/** `currentDocKey` is always built as `${docType}:${docId}` with both non-empty (see
|
|
125
|
+
* below), but that invariant isn't visible to `.split(':')`'s return type — parse it back
|
|
126
|
+
* out explicitly instead of asserting the shape with a cast. */
|
|
127
|
+
function parseDocKey(key: string): { collection: string; id: string } | null {
|
|
128
|
+
const separator = key.indexOf(':');
|
|
129
|
+
if (separator === -1) return null;
|
|
130
|
+
return { collection: key.slice(0, separator), id: key.slice(separator + 1) };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
let input = $state('');
|
|
134
|
+
let streaming = $state(false);
|
|
135
|
+
let controller: AbortController | null = null;
|
|
136
|
+
|
|
137
|
+
// The admin desk is a single `/admin` route driven by `docType`/`docId` query params
|
|
138
|
+
// (see AdminApp.svelte), not per-document URLs — this is how the assistant knows what's
|
|
139
|
+
// open without any data being pushed to it. Only a breadcrumb, never the document's
|
|
140
|
+
// content itself: the model calls `get_document` (already a registered tool) when it
|
|
141
|
+
// actually needs field values, so this stays cheap on every request and never goes stale.
|
|
142
|
+
const currentDocKey = $derived.by(() => {
|
|
143
|
+
const docType = page.url.searchParams.get('docType');
|
|
144
|
+
const docId = page.url.searchParams.get('docId');
|
|
145
|
+
return docType && docId ? `${docType}:${docId}` : null;
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// A URL param says *which* document is open; it's not a handle to mutate. Cross-check
|
|
149
|
+
// against document-workspace-registry.svelte.ts — the live editor tab's own registration —
|
|
150
|
+
// so a stale/mismatched registration (e.g. from a just-closed tab) never gets bridged into.
|
|
151
|
+
// Only when both agree does this chat gain the content_patch_fields/content_save_draft
|
|
152
|
+
// tools; everywhere else (no document open, or a URL that doesn't match what's registered)
|
|
153
|
+
// it behaves exactly as a general chat always has.
|
|
154
|
+
const matchedWorkspace = $derived.by(() => {
|
|
155
|
+
if (!currentDocKey) return null;
|
|
156
|
+
const [docType, docId] = currentDocKey.split(':');
|
|
157
|
+
const registered = documentWorkspaceRegistry.current;
|
|
158
|
+
if (registered && registered.documentId === docId && registered.collection === docType) {
|
|
159
|
+
return registered.workspace;
|
|
160
|
+
}
|
|
161
|
+
return null;
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
/** No schema context is threaded down to this global panel (schema context is scoped to
|
|
165
|
+
* DocumentEditor.svelte's own subtree) — resolving via conventional field names only
|
|
166
|
+
* (`resolvePreviewTitle`'s no-schema fallback path) rather than a schema's own
|
|
167
|
+
* `preview.prepare`/`select` config. Good enough for a breadcrumb chip; not worth wiring
|
|
168
|
+
* schema context app-wide just for this. */
|
|
169
|
+
function capitalize(s: string): string {
|
|
170
|
+
return s.length > 0 ? s.charAt(0).toUpperCase() + s.slice(1) : s;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Cursor-style persistent context chip: shows what's currently attached, live, distinct
|
|
174
|
+
// from the conversation transcript. Three states — no doc open (null, nothing rendered),
|
|
175
|
+
// doc open but not bridged (editing: false, read-only awareness only), doc open and
|
|
176
|
+
// bridged (editing: true, content_patch_fields/content_save_draft are live).
|
|
177
|
+
const documentLabel = $derived.by(() => {
|
|
178
|
+
if (!currentDocKey) return null;
|
|
179
|
+
const parsed = parseDocKey(currentDocKey);
|
|
180
|
+
if (!parsed) return null;
|
|
181
|
+
const typeLabel = capitalize(parsed.collection);
|
|
182
|
+
const workspace = matchedWorkspace;
|
|
183
|
+
if (!workspace) return { editing: false, text: typeLabel };
|
|
184
|
+
const title = resolvePreviewTitle(workspace.getSnapshot().data, null, typeLabel);
|
|
185
|
+
return { editing: true, text: `${typeLabel} — ${title}` };
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// Soft nudge, not a hard limit — this endpoint resends the full conversation every call,
|
|
189
|
+
// so `lastPromptTokens` only grows. A conservative threshold since the actual context
|
|
190
|
+
// window varies by provider/model and isn't known client-side.
|
|
191
|
+
const LONG_CONVERSATION_TOKENS = 50_000;
|
|
192
|
+
const isGettingLong = $derived(agentChatState.lastPromptTokens > LONG_CONVERSATION_TOKENS);
|
|
193
|
+
|
|
194
|
+
async function send(message = input.trim()) {
|
|
195
|
+
if (!message || streaming) return;
|
|
196
|
+
|
|
197
|
+
input = '';
|
|
198
|
+
if (currentDocKey && currentDocKey !== agentChatState.contextSentFor) {
|
|
199
|
+
const [docType, docId] = currentDocKey.split(':');
|
|
200
|
+
agentChatState.history.push({
|
|
201
|
+
role: 'system',
|
|
202
|
+
content: matchedWorkspace
|
|
203
|
+
? `The user is currently viewing document ${docId} in collection ${docType} in the editor. Use content_patch_fields/content_save_draft for edits to this document — do not use update_document for it, since only the workspace tools keep the open editor in sync.`
|
|
204
|
+
: `The user is currently viewing document ${docId} in collection ${docType}.`
|
|
205
|
+
});
|
|
206
|
+
agentChatState.contextSentFor = currentDocKey;
|
|
207
|
+
}
|
|
208
|
+
const userTurn: Turn = {
|
|
209
|
+
id: crypto.randomUUID(),
|
|
210
|
+
role: 'user',
|
|
211
|
+
text: message,
|
|
212
|
+
status: 'complete',
|
|
213
|
+
toolCalls: [],
|
|
214
|
+
historyIndexBeforeTurn: agentChatState.history.length
|
|
215
|
+
};
|
|
216
|
+
const assistantTurn: Turn = {
|
|
217
|
+
id: crypto.randomUUID(),
|
|
218
|
+
role: 'assistant',
|
|
219
|
+
text: '',
|
|
220
|
+
status: 'streaming',
|
|
221
|
+
toolCalls: []
|
|
222
|
+
};
|
|
223
|
+
agentChatState.turns.push(userTurn, assistantTurn);
|
|
224
|
+
agentChatState.history.push({ role: 'user', content: message });
|
|
225
|
+
const assistantIndex = agentChatState.turns.length - 1;
|
|
226
|
+
streaming = true;
|
|
227
|
+
controller = new AbortController();
|
|
228
|
+
|
|
229
|
+
// Captured once, at the start of this turn — even if the registered document changes
|
|
230
|
+
// mid-turn (navigation), this turn keeps bridging into the document it started with,
|
|
231
|
+
// not whatever happens to be open when a later leg resumes.
|
|
232
|
+
const workspace = matchedWorkspace;
|
|
233
|
+
const documentContext = workspace && currentDocKey ? parseDocKey(currentDocKey) : null;
|
|
234
|
+
|
|
235
|
+
let changeSetId: string | null = null;
|
|
236
|
+
let cumulativeUsage = { promptTokens: 0, completionTokens: 0 };
|
|
237
|
+
// Set by content_patch_fields, cleared by content_save_draft — if a turn ends with
|
|
238
|
+
// buffered-but-unflushed patches (the model forgot to call content_save_draft), the
|
|
239
|
+
// safety net below flushes them once rather than leaving them stranded unsaved.
|
|
240
|
+
let dirtySinceFlush = false;
|
|
241
|
+
|
|
242
|
+
if (workspace) workspace.beginBatch('agent');
|
|
243
|
+
|
|
244
|
+
try {
|
|
245
|
+
let messages: HistoryMessage[] = agentChatState.history;
|
|
246
|
+
let turnDone = false;
|
|
247
|
+
|
|
248
|
+
while (!turnDone) {
|
|
249
|
+
let pendingWorkspaceCalls:
|
|
250
|
+
| { toolCallId: string; name: string; arguments: Record<string, unknown> }[]
|
|
251
|
+
| null = null;
|
|
252
|
+
|
|
253
|
+
for await (const event of streamAgentChat(
|
|
254
|
+
{
|
|
255
|
+
messages,
|
|
256
|
+
documentContext: documentContext ?? undefined,
|
|
257
|
+
changeSetId: changeSetId ?? undefined,
|
|
258
|
+
priorUsage: changeSetId ? cumulativeUsage : undefined
|
|
259
|
+
},
|
|
260
|
+
controller.signal
|
|
261
|
+
)) {
|
|
262
|
+
if (event.type === 'usage') {
|
|
263
|
+
cumulativeUsage = {
|
|
264
|
+
promptTokens: event.promptTokens,
|
|
265
|
+
completionTokens: event.completionTokens
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
if (event.type === 'done') {
|
|
269
|
+
changeSetId = event.changeSetId ?? changeSetId;
|
|
270
|
+
messages = event.messages;
|
|
271
|
+
agentChatState.history = messages;
|
|
272
|
+
if (event.finishReason === 'awaiting_workspace_tool') {
|
|
273
|
+
pendingWorkspaceCalls = event.pendingWorkspaceCalls ?? [];
|
|
274
|
+
} else {
|
|
275
|
+
turnDone = true;
|
|
276
|
+
const turn = agentChatState.turns[assistantIndex];
|
|
277
|
+
if (turn?.status === 'streaming') {
|
|
278
|
+
turn.status = event.finishReason === 'error' ? 'error' : 'complete';
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
} else {
|
|
282
|
+
handleStreamEvent(assistantIndex, event);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (pendingWorkspaceCalls && pendingWorkspaceCalls.length > 0) {
|
|
287
|
+
for (const call of pendingWorkspaceCalls) {
|
|
288
|
+
const handler = workspace ? WORKSPACE_TOOL_HANDLERS[call.name] : undefined;
|
|
289
|
+
const result: WorkspaceToolHandlerResult = handler
|
|
290
|
+
? await handler(call.arguments, workspace!)
|
|
291
|
+
: {
|
|
292
|
+
success: false,
|
|
293
|
+
error: workspace
|
|
294
|
+
? `Unknown workspace tool: ${call.name}`
|
|
295
|
+
: 'No document is open to apply this change to.'
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
if (call.name === 'content_patch_fields') dirtySinceFlush = result.success;
|
|
299
|
+
if (call.name === 'content_save_draft') {
|
|
300
|
+
dirtySinceFlush = false;
|
|
301
|
+
// Resolved entirely client-side, so handleStreamEvent never sees it —
|
|
302
|
+
// invalidate the list here too, same as the server-mode tools above.
|
|
303
|
+
if (result.success && documentContext) {
|
|
304
|
+
notifyCollectionChanged(documentContext.collection);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
resolveWorkspaceToolCall(assistantIndex, call.toolCallId, result);
|
|
309
|
+
messages = [
|
|
310
|
+
...messages,
|
|
311
|
+
{
|
|
312
|
+
role: 'tool',
|
|
313
|
+
toolCallId: call.toolCallId,
|
|
314
|
+
content: JSON.stringify(
|
|
315
|
+
result.success ? (result.data ?? null) : { error: result.error }
|
|
316
|
+
)
|
|
317
|
+
}
|
|
318
|
+
];
|
|
319
|
+
|
|
320
|
+
if (changeSetId && documentContext && WORKSPACE_TOOLS_RECORDED.has(call.name)) {
|
|
321
|
+
await safeRecordWorkspaceOperation({
|
|
322
|
+
changeSetId,
|
|
323
|
+
toolName: call.name,
|
|
324
|
+
collection: documentContext.collection,
|
|
325
|
+
id: documentContext.id,
|
|
326
|
+
success: result.success,
|
|
327
|
+
error: result.error,
|
|
328
|
+
arguments: call.arguments,
|
|
329
|
+
data: result.data
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
agentChatState.history = messages;
|
|
334
|
+
} else if (!turnDone) {
|
|
335
|
+
// Defensive: the stream ended (e.g. aborted) without a 'done' event at all.
|
|
336
|
+
turnDone = true;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (dirtySinceFlush && workspace) {
|
|
341
|
+
const result = await saveDraftHandler({}, workspace);
|
|
342
|
+
if (result.success && documentContext) notifyCollectionChanged(documentContext.collection);
|
|
343
|
+
if (changeSetId && documentContext) {
|
|
344
|
+
await safeRecordWorkspaceOperation({
|
|
345
|
+
changeSetId,
|
|
346
|
+
toolName: 'content_save_draft',
|
|
347
|
+
collection: documentContext.collection,
|
|
348
|
+
id: documentContext.id,
|
|
349
|
+
success: result.success,
|
|
350
|
+
error: result.error,
|
|
351
|
+
arguments: {},
|
|
352
|
+
data: result.data
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
} catch (error) {
|
|
357
|
+
const turn = agentChatState.turns[assistantIndex];
|
|
358
|
+
if (!turn) return;
|
|
359
|
+
if (error instanceof DOMException && error.name === 'AbortError') {
|
|
360
|
+
turn.status = 'stopped';
|
|
361
|
+
} else {
|
|
362
|
+
turn.status = 'error';
|
|
363
|
+
turn.error = error instanceof Error ? error.message : String(error);
|
|
364
|
+
}
|
|
365
|
+
} finally {
|
|
366
|
+
if (workspace) workspace.endBatch();
|
|
367
|
+
streaming = false;
|
|
368
|
+
controller = null;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Server-mode tools that create/update/publish a document by `collection` — a collection
|
|
373
|
+
// list open elsewhere in the session (AdminApp) has no way to know about these on its own,
|
|
374
|
+
// same class of staleness as the single-document editor case. All three take `collection`
|
|
375
|
+
// as an argument, so the tool call's own arguments are enough to know what to invalidate —
|
|
376
|
+
// no need to inspect the (differently-shaped) result data of each.
|
|
377
|
+
const LIST_INVALIDATING_TOOLS = new Set([
|
|
378
|
+
'create_document',
|
|
379
|
+
'update_document',
|
|
380
|
+
'publish_document'
|
|
381
|
+
]);
|
|
382
|
+
|
|
383
|
+
function handleStreamEvent(assistantIndex: number, event: AgentStreamEvent) {
|
|
384
|
+
const turn = agentChatState.turns[assistantIndex];
|
|
385
|
+
if (!turn) return;
|
|
386
|
+
if (event.type === 'text') {
|
|
387
|
+
turn.text += event.delta;
|
|
388
|
+
} else if (event.type === 'toolCall') {
|
|
389
|
+
turn.toolCalls.push({
|
|
390
|
+
id: event.toolCallId,
|
|
391
|
+
name: event.name,
|
|
392
|
+
arguments: event.arguments,
|
|
393
|
+
status: 'running'
|
|
394
|
+
});
|
|
395
|
+
} else if (event.type === 'toolResult') {
|
|
396
|
+
const tool = turn.toolCalls.find((call) => call.id === event.toolCallId);
|
|
397
|
+
if (tool) {
|
|
398
|
+
tool.status = event.success ? 'complete' : 'error';
|
|
399
|
+
tool.result = event.data;
|
|
400
|
+
tool.error = event.error;
|
|
401
|
+
if (event.success && LIST_INVALIDATING_TOOLS.has(tool.name)) {
|
|
402
|
+
const collection = tool.arguments?.collection;
|
|
403
|
+
if (typeof collection === 'string') notifyCollectionChanged(collection);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
} else if (event.type === 'error') {
|
|
407
|
+
turn.status = 'error';
|
|
408
|
+
turn.error = event.message;
|
|
409
|
+
} else if (event.type === 'usage') {
|
|
410
|
+
// The endpoint resends the full conversation on every call, so the latest
|
|
411
|
+
// promptTokens is effectively "how big the conversation currently is" — used to
|
|
412
|
+
// warn before it gets unwieldy. Overwrite, don't sum: a turn can report usage
|
|
413
|
+
// multiple times across tool-calling round trips, and each one already reflects
|
|
414
|
+
// the whole conversation up to that point, not an incremental slice.
|
|
415
|
+
agentChatState.lastPromptTokens = event.promptTokens;
|
|
416
|
+
}
|
|
417
|
+
// 'done' is handled by send()'s own loop, not here — unlike every other event, it has
|
|
418
|
+
// to *decide* something (pause-and-resume vs. actually finish) that only the loop
|
|
419
|
+
// driving the fetch calls can act on; this function only ever does passive display
|
|
420
|
+
// updates for events streamed within one leg.
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/** Reflects a workspace tool's locally-resolved result onto its (already-rendered, still
|
|
424
|
+
* "running") entry in the turn's tool-call list — the SSE stream already emitted the
|
|
425
|
+
* `toolCall` event for it (before the server paused), just not a `toolResult`, since only
|
|
426
|
+
* the browser can produce that result. Same shape update as the `toolResult` branch above. */
|
|
427
|
+
function resolveWorkspaceToolCall(
|
|
428
|
+
assistantIndex: number,
|
|
429
|
+
toolCallId: string,
|
|
430
|
+
result: WorkspaceToolHandlerResult
|
|
431
|
+
) {
|
|
432
|
+
const turn = agentChatState.turns[assistantIndex];
|
|
433
|
+
const tool = turn?.toolCalls.find((call) => call.id === toolCallId);
|
|
434
|
+
if (tool) {
|
|
435
|
+
tool.status = result.success ? 'complete' : 'error';
|
|
436
|
+
tool.result = result.data;
|
|
437
|
+
tool.error = result.error;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/** Best-effort audit record for a workspace tool's result — `recordWorkspaceOperation`
|
|
442
|
+
* (api/agent-chat.ts) throws on any HTTP/network failure (it goes through `apiClient`); this
|
|
443
|
+
* call must never break the chat turn just because the audit write failed. */
|
|
444
|
+
async function safeRecordWorkspaceOperation(
|
|
445
|
+
body: Parameters<typeof recordWorkspaceOperation>[0]
|
|
446
|
+
) {
|
|
447
|
+
try {
|
|
448
|
+
await recordWorkspaceOperation(body);
|
|
449
|
+
} catch {
|
|
450
|
+
// best-effort — see doc comment above
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function stop() {
|
|
455
|
+
controller?.abort();
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function clearConversation() {
|
|
459
|
+
if (streaming) return;
|
|
460
|
+
// This wipes the persisted copy too (agent-chat-state.svelte.ts mirrors every change to
|
|
461
|
+
// localStorage), so confirm — unlike before that persistence existed, this is no longer
|
|
462
|
+
// trivially recoverable by just reopening the panel.
|
|
463
|
+
if (!confirm('Clear this conversation? This cannot be undone.')) return;
|
|
464
|
+
agentChatState.turns = [];
|
|
465
|
+
agentChatState.history = [];
|
|
466
|
+
agentChatState.contextSentFor = null;
|
|
467
|
+
agentChatState.lastPromptTokens = 0;
|
|
468
|
+
input = '';
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function retry(turnIndex: number) {
|
|
472
|
+
if (streaming || turnIndex < 1) return;
|
|
473
|
+
const prompt = agentChatState.turns[turnIndex - 1];
|
|
474
|
+
if (!prompt || prompt.role !== 'user') return;
|
|
475
|
+
agentChatState.turns = agentChatState.turns.slice(0, turnIndex - 1);
|
|
476
|
+
// A turn's server-reported `messages` can add more than the naive "2 entries" once tool
|
|
477
|
+
// calls are involved, so roll back to the exact length recorded before this turn rather
|
|
478
|
+
// than assuming an offset.
|
|
479
|
+
agentChatState.history = agentChatState.history.slice(
|
|
480
|
+
0,
|
|
481
|
+
prompt.historyIndexBeforeTurn ?? agentChatState.history.length
|
|
482
|
+
);
|
|
483
|
+
send(prompt.text);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
async function copyText(text: string) {
|
|
487
|
+
await navigator.clipboard.writeText(text);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
marked.setOptions({ breaks: true, gfm: true });
|
|
491
|
+
|
|
492
|
+
// Assistant text is model-generated markdown, not trusted HTML — parse then sanitize before
|
|
493
|
+
// ever using `{@html}`. `marked.parse` is sync here (no async extensions registered).
|
|
494
|
+
function renderMarkdown(text: string): string {
|
|
495
|
+
return DOMPurify.sanitize(marked.parse(text, { async: false }));
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function formatToolName(name: string) {
|
|
499
|
+
return name.replaceAll('_', ' ');
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// Past-tense phrase per tool, for both the one-line turn summary ("Listed collections,
|
|
503
|
+
// updated a document") and each entry's title in the expanded detail.
|
|
504
|
+
const TOOL_VERBS: Record<string, string> = {
|
|
505
|
+
describe_cms: 'inspected the CMS',
|
|
506
|
+
list_collections: 'listed collections',
|
|
507
|
+
get_schema: 'read a schema',
|
|
508
|
+
validate_schema: 'validated a schema',
|
|
509
|
+
validate_document: 'validated a document',
|
|
510
|
+
query_documents: 'searched documents',
|
|
511
|
+
get_document: 'read a document',
|
|
512
|
+
create_document: 'created a document',
|
|
513
|
+
update_document: 'updated a document',
|
|
514
|
+
publish_document: 'published a document',
|
|
515
|
+
get_singleton: 'read settings',
|
|
516
|
+
update_singleton: 'updated settings',
|
|
517
|
+
list_assets: 'browsed assets',
|
|
518
|
+
upload_asset: 'uploaded a file'
|
|
519
|
+
};
|
|
520
|
+
|
|
521
|
+
const TOOL_ICONS: Record<string, Component> = {
|
|
522
|
+
describe_cms: Info,
|
|
523
|
+
list_collections: Layers,
|
|
524
|
+
get_schema: FileCode,
|
|
525
|
+
validate_schema: ShieldCheck,
|
|
526
|
+
validate_document: ShieldCheck,
|
|
527
|
+
query_documents: Search,
|
|
528
|
+
get_document: FileText,
|
|
529
|
+
create_document: FilePlus,
|
|
530
|
+
update_document: Pencil,
|
|
531
|
+
publish_document: UploadCloud,
|
|
532
|
+
get_singleton: Settings,
|
|
533
|
+
update_singleton: Settings,
|
|
534
|
+
list_assets: Image,
|
|
535
|
+
upload_asset: ImagePlus
|
|
536
|
+
};
|
|
537
|
+
|
|
538
|
+
function toolVerb(name: string) {
|
|
539
|
+
return TOOL_VERBS[name] ?? formatToolName(name);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function toolIcon(name: string): Component {
|
|
543
|
+
return TOOL_ICONS[name] ?? Wrench;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function toolTitle(tool: ToolCall) {
|
|
547
|
+
const verb = toolVerb(tool.name);
|
|
548
|
+
return verb.charAt(0).toUpperCase() + verb.slice(1);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/** One line of context under a tool entry — where it looked/wrote, and a result count if
|
|
552
|
+
* the tool's own result shape has one. Kept generic across tools rather than special-cased
|
|
553
|
+
* per name, since new tools shouldn't need a matching branch here to show *something*. */
|
|
554
|
+
function plural(n: number, word: string) {
|
|
555
|
+
return `${n} ${word}${n === 1 ? '' : 's'}`;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/** A result note per tool, named to its actual result shape — not a generic "find the
|
|
559
|
+
* first array field" guess, which produced nonsense (e.g. counting validate_document's
|
|
560
|
+
* `errors` array as "results", or grabbing an arbitrary field off describe_cms). Tools
|
|
561
|
+
* whose result has nothing meaningful to summarize just get no note (the location line
|
|
562
|
+
* — collection/id — still shows). */
|
|
563
|
+
function toolResultNote(tool: ToolCall): string | null {
|
|
564
|
+
const r = tool.result;
|
|
565
|
+
if (!r || typeof r !== 'object') return null;
|
|
566
|
+
const record = r as Record<string, unknown>;
|
|
567
|
+
|
|
568
|
+
switch (tool.name) {
|
|
569
|
+
case 'list_collections': {
|
|
570
|
+
const n = (record.collections as unknown[] | undefined)?.length;
|
|
571
|
+
return n === undefined ? null : plural(n, 'collection');
|
|
572
|
+
}
|
|
573
|
+
case 'list_assets': {
|
|
574
|
+
const n = (record.assets as unknown[] | undefined)?.length;
|
|
575
|
+
return n === undefined ? null : plural(n, 'asset');
|
|
576
|
+
}
|
|
577
|
+
case 'query_documents': {
|
|
578
|
+
const total = record.totalDocs;
|
|
579
|
+
return typeof total === 'number' ? plural(total, 'document') : null;
|
|
580
|
+
}
|
|
581
|
+
case 'validate_document':
|
|
582
|
+
case 'validate_schema': {
|
|
583
|
+
if (record.isValid === true) return 'valid';
|
|
584
|
+
const n = (record.errors as unknown[] | undefined)?.length ?? 0;
|
|
585
|
+
return plural(n, 'error');
|
|
586
|
+
}
|
|
587
|
+
default:
|
|
588
|
+
return null;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function toolDetail(tool: ToolCall): string | null {
|
|
593
|
+
const parts: string[] = [];
|
|
594
|
+
const collection = tool.arguments.collection;
|
|
595
|
+
const id = tool.arguments.id;
|
|
596
|
+
if (typeof collection === 'string') parts.push(collection);
|
|
597
|
+
if (typeof id === 'string') parts.push(id);
|
|
598
|
+
const location = parts.join(' · ');
|
|
599
|
+
|
|
600
|
+
return [location, toolResultNote(tool)].filter(Boolean).join(' — ') || null;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// The turn's collapsed summary line — unique tool verbs in first-use order, e.g.
|
|
604
|
+
// "Listed collections, updated a document".
|
|
605
|
+
function toolsSummary(turn: Turn): string {
|
|
606
|
+
const seen = new Set<string>();
|
|
607
|
+
const verbs: string[] = [];
|
|
608
|
+
for (const tool of turn.toolCalls) {
|
|
609
|
+
if (seen.has(tool.name)) continue;
|
|
610
|
+
seen.add(tool.name);
|
|
611
|
+
verbs.push(toolVerb(tool.name));
|
|
612
|
+
}
|
|
613
|
+
const sentence = verbs.join(', ');
|
|
614
|
+
return sentence.charAt(0).toUpperCase() + sentence.slice(1);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function streamingLabel(turn: Turn) {
|
|
618
|
+
if (turn.toolCalls.some((tool) => tool.status === 'running')) return 'Using tools...';
|
|
619
|
+
if (turn.text) return 'Generating response...';
|
|
620
|
+
return 'Thinking...';
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function handleComposerKeydown(event: KeyboardEvent) {
|
|
624
|
+
if (event.key === 'Enter' && !event.shiftKey && !event.isComposing) {
|
|
625
|
+
event.preventDefault();
|
|
626
|
+
send();
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
</script>
|
|
630
|
+
|
|
631
|
+
<div class="bg-muted/20 flex h-full min-h-0 flex-col">
|
|
632
|
+
<header
|
|
633
|
+
class="bg-background/90 relative flex h-14 shrink-0 items-center justify-between border-b px-4 backdrop-blur sm:px-6 {embedded
|
|
634
|
+
? 'pr-20 sm:pr-20'
|
|
635
|
+
: ''}"
|
|
636
|
+
>
|
|
637
|
+
<div class="flex min-w-0 items-center gap-3">
|
|
638
|
+
<div
|
|
639
|
+
class="bg-primary/10 text-primary flex size-8 shrink-0 items-center justify-center rounded-lg"
|
|
640
|
+
>
|
|
641
|
+
<Sparkles class="size-4" />
|
|
642
|
+
</div>
|
|
643
|
+
<div class="min-w-0">
|
|
644
|
+
<h1 class="truncate text-sm font-semibold">{title}</h1>
|
|
645
|
+
<p class="text-muted-foreground truncate text-xs">{subtitle}</p>
|
|
646
|
+
</div>
|
|
647
|
+
</div>
|
|
648
|
+
{#if agentChatState.turns.length > 0}
|
|
649
|
+
<Button
|
|
650
|
+
variant="ghost"
|
|
651
|
+
size="icon-sm"
|
|
652
|
+
onclick={clearConversation}
|
|
653
|
+
disabled={streaming}
|
|
654
|
+
aria-label="Clear conversation"
|
|
655
|
+
class={embedded ? 'absolute top-2 right-14' : ''}
|
|
656
|
+
>
|
|
657
|
+
<Trash2 />
|
|
658
|
+
</Button>
|
|
659
|
+
{/if}
|
|
660
|
+
</header>
|
|
661
|
+
|
|
662
|
+
<MessageScrollerProvider
|
|
663
|
+
autoScroll
|
|
664
|
+
defaultScrollPosition="last-anchor"
|
|
665
|
+
scrollPreviousItemPeek={48}
|
|
666
|
+
>
|
|
667
|
+
<MessageScroller class="min-h-0">
|
|
668
|
+
<MessageScrollerViewport>
|
|
669
|
+
<MessageScrollerContent
|
|
670
|
+
class="mx-auto w-full max-w-3xl px-4 py-8 sm:px-6"
|
|
671
|
+
aria-busy={streaming}
|
|
672
|
+
>
|
|
673
|
+
{#if agentChatState.turns.length === 0}
|
|
674
|
+
<div class="m-auto flex max-w-xl flex-col items-center px-4 py-12 text-center">
|
|
675
|
+
<div
|
|
676
|
+
class="border-primary/20 bg-primary/10 text-primary mb-5 flex size-14 items-center justify-center rounded-2xl border shadow-sm"
|
|
677
|
+
>
|
|
678
|
+
<Bot class="size-7" />
|
|
679
|
+
</div>
|
|
680
|
+
<h2 class="text-xl font-semibold tracking-tight">What are we working on?</h2>
|
|
681
|
+
<p class="text-muted-foreground mt-2 max-w-md text-sm leading-relaxed">
|
|
682
|
+
Ask about your schemas, inspect documents, or use the CMS tools available to your
|
|
683
|
+
role.
|
|
684
|
+
</p>
|
|
685
|
+
<div class="mt-6 grid w-full gap-2 sm:grid-cols-3">
|
|
686
|
+
{#each suggestions as suggestion}
|
|
687
|
+
{@const text = typeof suggestion === 'string' ? suggestion : suggestion.text}
|
|
688
|
+
{@const Icon = typeof suggestion === 'string' ? null : suggestion.icon}
|
|
689
|
+
<button
|
|
690
|
+
type="button"
|
|
691
|
+
class="bg-background hover:bg-accent flex cursor-pointer items-start gap-2 rounded-xl border p-3 text-left text-xs leading-relaxed shadow-xs transition-colors"
|
|
692
|
+
onclick={() => send(text)}
|
|
693
|
+
>
|
|
694
|
+
{#if Icon}
|
|
695
|
+
<Icon class="text-muted-foreground mt-0.5 size-3.5 shrink-0" />
|
|
696
|
+
{/if}
|
|
697
|
+
<span>{text}</span>
|
|
698
|
+
</button>
|
|
699
|
+
{/each}
|
|
700
|
+
</div>
|
|
701
|
+
</div>
|
|
702
|
+
{:else}
|
|
703
|
+
{#each agentChatState.turns as turn, index (turn.id)}
|
|
704
|
+
<MessageScrollerItem messageId={turn.id} scrollAnchor={turn.role === 'user'}>
|
|
705
|
+
<Message align={turn.role === 'user' ? 'end' : 'start'}>
|
|
706
|
+
<MessageContent>
|
|
707
|
+
{#if turn.role === 'user'}
|
|
708
|
+
<Bubble variant="secondary" align="end"
|
|
709
|
+
><BubbleContent class="rounded-xl">{turn.text}</BubbleContent></Bubble
|
|
710
|
+
>
|
|
711
|
+
{:else}
|
|
712
|
+
{#if turn.toolCalls.length > 0}
|
|
713
|
+
<details class="group/tools mb-2 w-full">
|
|
714
|
+
<summary
|
|
715
|
+
class="text-muted-foreground hover:text-foreground flex cursor-pointer list-none items-center gap-1.5 text-xs transition-colors"
|
|
716
|
+
>
|
|
717
|
+
<span
|
|
718
|
+
class={turn.toolCalls.some((t) => t.status === 'running')
|
|
719
|
+
? 'shimmer'
|
|
720
|
+
: ''}>{toolsSummary(turn)}</span
|
|
721
|
+
>
|
|
722
|
+
<ChevronDown
|
|
723
|
+
class="size-3 transition-transform group-open/tools:rotate-180"
|
|
724
|
+
/>
|
|
725
|
+
</summary>
|
|
726
|
+
<div
|
|
727
|
+
class="bg-muted/40 mt-1.5 flex flex-col gap-2.5 rounded-lg px-2.5 py-2"
|
|
728
|
+
>
|
|
729
|
+
{#each turn.toolCalls as tool (tool.id)}
|
|
730
|
+
{@const Icon = toolIcon(tool.name)}
|
|
731
|
+
{@const detail = toolDetail(tool)}
|
|
732
|
+
<div class="flex flex-col gap-0.5">
|
|
733
|
+
<div class="text-muted-foreground flex items-center gap-2 text-xs">
|
|
734
|
+
{#if tool.status === 'running'}
|
|
735
|
+
<LoaderCircle class="size-3 shrink-0 animate-spin" />
|
|
736
|
+
{:else if tool.status === 'error'}
|
|
737
|
+
<CircleAlert class="text-destructive size-3 shrink-0" />
|
|
738
|
+
{:else}
|
|
739
|
+
<Icon class="size-3 shrink-0" />
|
|
740
|
+
{/if}
|
|
741
|
+
<span>{toolTitle(tool)}</span>
|
|
742
|
+
</div>
|
|
743
|
+
{#if tool.error}
|
|
744
|
+
<p class="text-destructive pl-5 text-xs">{tool.error}</p>
|
|
745
|
+
{:else if detail}
|
|
746
|
+
<p class="text-muted-foreground/80 pl-5 text-xs">{detail}</p>
|
|
747
|
+
{/if}
|
|
748
|
+
</div>
|
|
749
|
+
{/each}
|
|
750
|
+
<div class="text-muted-foreground flex items-center gap-2 text-xs">
|
|
751
|
+
{#if turn.toolCalls.some((t) => t.status === 'running')}
|
|
752
|
+
<LoaderCircle class="size-3 animate-spin" /> Working…
|
|
753
|
+
{:else if turn.toolCalls.some((t) => t.status === 'error')}
|
|
754
|
+
<CircleAlert class="text-destructive size-3" /> Done with errors
|
|
755
|
+
{:else}
|
|
756
|
+
<Check class="size-3" /> Done
|
|
757
|
+
{/if}
|
|
758
|
+
</div>
|
|
759
|
+
</div>
|
|
760
|
+
</details>
|
|
761
|
+
{/if}
|
|
762
|
+
|
|
763
|
+
{#if turn.text}
|
|
764
|
+
<Bubble variant="ghost"
|
|
765
|
+
><BubbleContent class="markdown-body px-0"
|
|
766
|
+
>{@html renderMarkdown(turn.text)}</BubbleContent
|
|
767
|
+
></Bubble
|
|
768
|
+
>
|
|
769
|
+
{/if}
|
|
770
|
+
{#if turn.status === 'streaming'}
|
|
771
|
+
<Marker
|
|
772
|
+
role="status"
|
|
773
|
+
class="bg-muted/60 mt-1 w-fit justify-start rounded-full px-3 py-1.5"
|
|
774
|
+
>
|
|
775
|
+
<MarkerIcon><LoaderCircle class="animate-spin" /></MarkerIcon>
|
|
776
|
+
<MarkerContent class="shimmer font-medium"
|
|
777
|
+
>{streamingLabel(turn)}</MarkerContent
|
|
778
|
+
>
|
|
779
|
+
</Marker>
|
|
780
|
+
{/if}
|
|
781
|
+
|
|
782
|
+
{#if turn.error}
|
|
783
|
+
<Marker variant="border" class="text-destructive border-destructive/30"
|
|
784
|
+
><MarkerIcon><CircleAlert /></MarkerIcon><MarkerContent
|
|
785
|
+
>{turn.error}</MarkerContent
|
|
786
|
+
></Marker
|
|
787
|
+
>
|
|
788
|
+
{/if}
|
|
789
|
+
<MessageFooter>
|
|
790
|
+
{#if turn.status === 'stopped'}<span>Stopped</span>{/if}
|
|
791
|
+
{#if turn.text && turn.status !== 'streaming'}
|
|
792
|
+
<Button
|
|
793
|
+
variant="ghost"
|
|
794
|
+
size="icon-sm"
|
|
795
|
+
class="size-7"
|
|
796
|
+
onclick={() => copyText(turn.text)}
|
|
797
|
+
aria-label="Copy response"><Copy /></Button
|
|
798
|
+
>
|
|
799
|
+
<Button
|
|
800
|
+
variant="ghost"
|
|
801
|
+
size="icon-sm"
|
|
802
|
+
class="size-7"
|
|
803
|
+
onclick={() => retry(index)}
|
|
804
|
+
disabled={streaming}
|
|
805
|
+
aria-label="Retry response"><RefreshCw /></Button
|
|
806
|
+
>
|
|
807
|
+
{/if}
|
|
808
|
+
</MessageFooter>
|
|
809
|
+
{/if}
|
|
810
|
+
</MessageContent>
|
|
811
|
+
</Message>
|
|
812
|
+
</MessageScrollerItem>
|
|
813
|
+
{/each}
|
|
814
|
+
{/if}
|
|
815
|
+
</MessageScrollerContent>
|
|
816
|
+
</MessageScrollerViewport>
|
|
817
|
+
<MessageScrollerButton />
|
|
818
|
+
</MessageScroller>
|
|
819
|
+
</MessageScrollerProvider>
|
|
820
|
+
|
|
821
|
+
<div
|
|
822
|
+
class="from-background via-background bg-linear-to-t to-transparent px-3 pt-2 pb-3 sm:px-6 sm:pb-5"
|
|
823
|
+
>
|
|
824
|
+
{#if documentLabel}
|
|
825
|
+
<div
|
|
826
|
+
class="mx-auto mb-2 flex max-w-3xl items-center gap-1.5 text-xs {documentLabel.editing
|
|
827
|
+
? 'text-primary'
|
|
828
|
+
: 'text-muted-foreground'}"
|
|
829
|
+
>
|
|
830
|
+
{#if documentLabel.editing}
|
|
831
|
+
<Pencil class="size-3.5 shrink-0" />
|
|
832
|
+
<span class="truncate font-medium">Editing: {documentLabel.text}</span>
|
|
833
|
+
{:else}
|
|
834
|
+
<FileText class="size-3.5 shrink-0" />
|
|
835
|
+
<span class="truncate">Viewing: {documentLabel.text}</span>
|
|
836
|
+
{/if}
|
|
837
|
+
</div>
|
|
838
|
+
{/if}
|
|
839
|
+
{#if isGettingLong}
|
|
840
|
+
<div
|
|
841
|
+
class="border-rule bg-muted/60 text-muted-foreground mx-auto mb-2 flex max-w-3xl items-center gap-2 rounded-lg border px-3 py-2 text-xs"
|
|
842
|
+
>
|
|
843
|
+
<CircleAlert class="size-3.5 shrink-0" />
|
|
844
|
+
<span class="flex-1"
|
|
845
|
+
>This conversation is getting long (~{Math.round(agentChatState.lastPromptTokens / 1000)}K
|
|
846
|
+
tokens sent each turn) — consider clearing it for better results.</span
|
|
847
|
+
>
|
|
848
|
+
<Button variant="ghost" size="sm" class="h-6 shrink-0 px-2" onclick={clearConversation}
|
|
849
|
+
>Clear</Button
|
|
850
|
+
>
|
|
851
|
+
</div>
|
|
852
|
+
{/if}
|
|
853
|
+
<form
|
|
854
|
+
class="bg-background focus-within:border-ring focus-within:ring-ring/20 mx-auto max-w-3xl rounded-2xl border p-2 shadow-lg transition-shadow focus-within:ring-3"
|
|
855
|
+
onsubmit={(event) => {
|
|
856
|
+
event.preventDefault();
|
|
857
|
+
send();
|
|
858
|
+
}}
|
|
859
|
+
>
|
|
860
|
+
<Textarea
|
|
861
|
+
bind:value={input}
|
|
862
|
+
onkeydown={handleComposerKeydown}
|
|
863
|
+
rows={1}
|
|
864
|
+
placeholder="Ask about your content..."
|
|
865
|
+
class="max-h-40 min-h-11 resize-none border-0 bg-transparent px-2 shadow-none focus-visible:ring-0"
|
|
866
|
+
disabled={streaming}
|
|
867
|
+
/>
|
|
868
|
+
<div class="flex items-center justify-between px-1 pt-1">
|
|
869
|
+
<div class="text-muted-foreground flex items-center gap-1.5 text-[11px]">
|
|
870
|
+
<Database class="size-3" /><span>Uses tools allowed by your role</span>
|
|
871
|
+
</div>
|
|
872
|
+
{#if streaming}
|
|
873
|
+
<Button
|
|
874
|
+
type="button"
|
|
875
|
+
size="icon-sm"
|
|
876
|
+
variant="outline"
|
|
877
|
+
onclick={stop}
|
|
878
|
+
aria-label="Stop response"><Square class="fill-current" /></Button
|
|
879
|
+
>
|
|
880
|
+
{:else}
|
|
881
|
+
<Button type="submit" size="icon-sm" disabled={!input.trim()} aria-label="Send message"
|
|
882
|
+
><Send /></Button
|
|
883
|
+
>
|
|
884
|
+
{/if}
|
|
885
|
+
</div>
|
|
886
|
+
</form>
|
|
887
|
+
<p class="text-muted-foreground mx-auto mt-2 max-w-3xl text-center text-[10px]">
|
|
888
|
+
AI can make mistakes. Review content changes before publishing.
|
|
889
|
+
</p>
|
|
890
|
+
</div>
|
|
891
|
+
</div>
|
|
892
|
+
|
|
893
|
+
<style>
|
|
894
|
+
/* `{@html}`-injected markdown bypasses Svelte's scoped-style attribute, so these rules
|
|
895
|
+
have to be :global — they only ever target `.markdown-body`, scoped by that class alone. */
|
|
896
|
+
:global(.markdown-body > :first-child) {
|
|
897
|
+
margin-top: 0;
|
|
898
|
+
}
|
|
899
|
+
:global(.markdown-body > :last-child) {
|
|
900
|
+
margin-bottom: 0;
|
|
901
|
+
}
|
|
902
|
+
:global(.markdown-body p) {
|
|
903
|
+
margin: 0.5em 0;
|
|
904
|
+
}
|
|
905
|
+
:global(.markdown-body ul),
|
|
906
|
+
:global(.markdown-body ol) {
|
|
907
|
+
margin: 0.5em 0;
|
|
908
|
+
padding-left: 1.25em;
|
|
909
|
+
}
|
|
910
|
+
:global(.markdown-body ul) {
|
|
911
|
+
list-style: disc;
|
|
912
|
+
}
|
|
913
|
+
:global(.markdown-body ol) {
|
|
914
|
+
list-style: decimal;
|
|
915
|
+
}
|
|
916
|
+
:global(.markdown-body li) {
|
|
917
|
+
margin: 0.15em 0;
|
|
918
|
+
}
|
|
919
|
+
:global(.markdown-body li > p) {
|
|
920
|
+
margin: 0;
|
|
921
|
+
}
|
|
922
|
+
:global(.markdown-body h1),
|
|
923
|
+
:global(.markdown-body h2),
|
|
924
|
+
:global(.markdown-body h3) {
|
|
925
|
+
font-weight: 600;
|
|
926
|
+
margin: 0.75em 0 0.35em;
|
|
927
|
+
}
|
|
928
|
+
:global(.markdown-body h1) {
|
|
929
|
+
font-size: 1.15em;
|
|
930
|
+
}
|
|
931
|
+
:global(.markdown-body h2) {
|
|
932
|
+
font-size: 1.08em;
|
|
933
|
+
}
|
|
934
|
+
:global(.markdown-body h3) {
|
|
935
|
+
font-size: 1.02em;
|
|
936
|
+
}
|
|
937
|
+
:global(.markdown-body a) {
|
|
938
|
+
color: var(--primary);
|
|
939
|
+
text-decoration: underline;
|
|
940
|
+
text-underline-offset: 2px;
|
|
941
|
+
}
|
|
942
|
+
:global(.markdown-body code) {
|
|
943
|
+
background: var(--muted);
|
|
944
|
+
padding: 0.1em 0.35em;
|
|
945
|
+
border-radius: 0.3em;
|
|
946
|
+
font-size: 0.85em;
|
|
947
|
+
}
|
|
948
|
+
:global(.markdown-body pre) {
|
|
949
|
+
background: var(--muted);
|
|
950
|
+
padding: 0.6em 0.75em;
|
|
951
|
+
border-radius: 0.5em;
|
|
952
|
+
overflow-x: auto;
|
|
953
|
+
margin: 0.5em 0;
|
|
954
|
+
}
|
|
955
|
+
:global(.markdown-body pre code) {
|
|
956
|
+
background: none;
|
|
957
|
+
padding: 0;
|
|
958
|
+
}
|
|
959
|
+
:global(.markdown-body blockquote) {
|
|
960
|
+
border-left: 2px solid var(--border);
|
|
961
|
+
padding-left: 0.75em;
|
|
962
|
+
margin: 0.5em 0;
|
|
963
|
+
color: var(--muted-foreground);
|
|
964
|
+
}
|
|
965
|
+
:global(.markdown-body table) {
|
|
966
|
+
border-collapse: collapse;
|
|
967
|
+
margin: 0.5em 0;
|
|
968
|
+
}
|
|
969
|
+
:global(.markdown-body th),
|
|
970
|
+
:global(.markdown-body td) {
|
|
971
|
+
border: 1px solid var(--border);
|
|
972
|
+
padding: 0.25em 0.5em;
|
|
973
|
+
}
|
|
974
|
+
:global(.markdown-body strong) {
|
|
975
|
+
font-weight: 600;
|
|
976
|
+
}
|
|
977
|
+
</style>
|