ruflo 3.10.36 → 3.10.38
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/README.md +416 -416
- package/bin/ruflo.js +77 -77
- package/package.json +113 -113
- package/src/chat-ui/Dockerfile +25 -25
- package/src/chat-ui/patch-mcp-url-safety.sh +28 -28
- package/src/config/config.example.json +76 -76
- package/src/mcp-bridge/Dockerfile +45 -45
- package/src/mcp-bridge/index.js +1692 -1692
- package/src/mcp-bridge/mcp-stdio-kernel.js +159 -159
- package/src/mcp-bridge/package.json +17 -17
- package/src/mcp-bridge/test-harness.js +470 -470
- package/src/nginx/Dockerfile +10 -10
- package/src/nginx/nginx.conf +67 -67
- package/src/nginx/static/favicon-dark.svg +4 -4
- package/src/nginx/static/favicon.svg +4 -4
- package/src/nginx/static/icon.svg +5 -5
- package/src/nginx/static/logo.svg +9 -9
- package/src/nginx/static/manifest.json +22 -22
- package/src/nginx/static/welcome.js +184 -184
- package/src/ruvocal/.claude/skills/add-model-descriptions/SKILL.md +73 -73
- package/src/ruvocal/.devcontainer/Dockerfile +9 -9
- package/src/ruvocal/.devcontainer/devcontainer.json +36 -36
- package/src/ruvocal/.dockerignore +16 -16
- package/src/ruvocal/.eslintignore +13 -13
- package/src/ruvocal/.eslintrc.cjs +45 -45
- package/src/ruvocal/.gcloudignore +18 -18
- package/src/ruvocal/.github/ISSUE_TEMPLATE/bug-report--chat-ui-.md +43 -43
- package/src/ruvocal/.github/ISSUE_TEMPLATE/config-support.md +9 -9
- package/src/ruvocal/.github/ISSUE_TEMPLATE/feature-request--chat-ui-.md +17 -17
- package/src/ruvocal/.github/ISSUE_TEMPLATE/huggingchat.md +11 -11
- package/src/ruvocal/.github/release.yml +16 -16
- package/src/ruvocal/.github/workflows/build-docs.yml +18 -18
- package/src/ruvocal/.github/workflows/build-image.yml +142 -142
- package/src/ruvocal/.github/workflows/build-pr-docs.yml +20 -20
- package/src/ruvocal/.github/workflows/deploy-dev.yml +63 -63
- package/src/ruvocal/.github/workflows/deploy-prod.yml +78 -78
- package/src/ruvocal/.github/workflows/lint-and-test.yml +84 -84
- package/src/ruvocal/.github/workflows/slugify.yaml +72 -72
- package/src/ruvocal/.github/workflows/trufflehog.yml +17 -17
- package/src/ruvocal/.github/workflows/upload-pr-documentation.yml +16 -16
- package/src/ruvocal/.husky/lint-stage-config.js +4 -4
- package/src/ruvocal/.husky/pre-commit +2 -2
- package/src/ruvocal/.prettierignore +14 -14
- package/src/ruvocal/.prettierrc +7 -7
- package/src/ruvocal/CLAUDE.md +126 -126
- package/src/ruvocal/Dockerfile +96 -96
- package/src/ruvocal/LICENSE +202 -202
- package/src/ruvocal/PRIVACY.md +41 -41
- package/src/ruvocal/README.md +164 -164
- package/src/ruvocal/chart/Chart.yaml +5 -5
- package/src/ruvocal/chart/env/dev.yaml +260 -260
- package/src/ruvocal/chart/env/prod.yaml +273 -273
- package/src/ruvocal/chart/templates/_helpers.tpl +22 -22
- package/src/ruvocal/chart/templates/config.yaml +10 -10
- package/src/ruvocal/chart/templates/deployment.yaml +81 -81
- package/src/ruvocal/chart/templates/hpa.yaml +45 -45
- package/src/ruvocal/chart/templates/infisical.yaml +24 -24
- package/src/ruvocal/chart/templates/ingress-internal.yaml +32 -32
- package/src/ruvocal/chart/templates/ingress.yaml +32 -32
- package/src/ruvocal/chart/templates/network-policy.yaml +36 -36
- package/src/ruvocal/chart/templates/service-account.yaml +13 -13
- package/src/ruvocal/chart/templates/service-monitor.yaml +17 -17
- package/src/ruvocal/chart/templates/service.yaml +21 -21
- package/src/ruvocal/chart/values.yaml +73 -73
- package/src/ruvocal/cloudbuild.yaml +68 -68
- package/src/ruvocal/config/branding.env.example +19 -19
- package/src/ruvocal/docker-compose.yml +21 -21
- package/src/ruvocal/docs/adr/ADR-029-HUGGINGFACE-CHAT-UI-CLOUD-RUN.md +1236 -1236
- package/src/ruvocal/docs/adr/ADR-033-RUVECTOR-RUFLO-MCP-INTEGRATION.md +111 -111
- package/src/ruvocal/docs/adr/ADR-034-OPTIONAL-MCP-BACKENDS.md +117 -117
- package/src/ruvocal/docs/adr/ADR-035-MCP-TOOL-GROUPS.md +186 -186
- package/src/ruvocal/docs/adr/ADR-037-AUTOPILOT-CHAT-MODE.md +1500 -1500
- package/src/ruvocal/docs/adr/ADR-038-RUVOCAL-FORK.md +286 -286
- package/src/ruvocal/docs/source/_toctree.yml +30 -30
- package/src/ruvocal/docs/source/configuration/common-issues.md +38 -38
- package/src/ruvocal/docs/source/configuration/llm-router.md +105 -105
- package/src/ruvocal/docs/source/configuration/mcp-tools.md +84 -84
- package/src/ruvocal/docs/source/configuration/metrics.md +9 -9
- package/src/ruvocal/docs/source/configuration/open-id.md +57 -57
- package/src/ruvocal/docs/source/configuration/overview.md +89 -89
- package/src/ruvocal/docs/source/configuration/theming.md +20 -20
- package/src/ruvocal/docs/source/developing/architecture.md +48 -48
- package/src/ruvocal/docs/source/index.md +53 -53
- package/src/ruvocal/docs/source/installation/docker.md +43 -43
- package/src/ruvocal/docs/source/installation/helm.md +43 -43
- package/src/ruvocal/docs/source/installation/local.md +62 -62
- package/src/ruvocal/entrypoint.sh +18 -18
- package/src/ruvocal/mcp-bridge/Dockerfile +45 -45
- package/src/ruvocal/mcp-bridge/cloudbuild.yaml +49 -49
- package/src/ruvocal/mcp-bridge/index.js +1902 -1902
- package/src/ruvocal/mcp-bridge/mcp-stdio-kernel.js +159 -159
- package/src/ruvocal/mcp-bridge/package-lock.json +762 -762
- package/src/ruvocal/mcp-bridge/package.json +17 -17
- package/src/ruvocal/mcp-bridge/test-harness.js +470 -470
- package/src/ruvocal/package-lock.json +11741 -11741
- package/src/ruvocal/package.json +121 -121
- package/src/ruvocal/postcss.config.js +6 -6
- package/src/ruvocal/rvf.manifest.json +204 -204
- package/src/ruvocal/scripts/config.ts +64 -64
- package/src/ruvocal/scripts/generate-welcome.mjs +181 -181
- package/src/ruvocal/scripts/populate.ts +288 -288
- package/src/ruvocal/scripts/samples.txt +194 -194
- package/src/ruvocal/scripts/setups/vitest-setup-server.ts +44 -44
- package/src/ruvocal/scripts/updateLocalEnv.ts +48 -48
- package/src/ruvocal/src/ambient.d.ts +7 -7
- package/src/ruvocal/src/app.d.ts +29 -29
- package/src/ruvocal/src/app.html +53 -53
- package/src/ruvocal/src/hooks.server.ts +32 -32
- package/src/ruvocal/src/hooks.ts +6 -6
- package/src/ruvocal/src/lib/APIClient.ts +148 -148
- package/src/ruvocal/src/lib/actions/clickOutside.ts +18 -18
- package/src/ruvocal/src/lib/actions/snapScrollToBottom.ts +346 -346
- package/src/ruvocal/src/lib/buildPrompt.ts +33 -33
- package/src/ruvocal/src/lib/components/AnnouncementBanner.svelte +20 -20
- package/src/ruvocal/src/lib/components/BackgroundGenerationPoller.svelte +168 -168
- package/src/ruvocal/src/lib/components/CodeBlock.svelte +73 -73
- package/src/ruvocal/src/lib/components/CopyToClipBoardBtn.svelte +92 -92
- package/src/ruvocal/src/lib/components/DeleteConversationModal.svelte +75 -75
- package/src/ruvocal/src/lib/components/EditConversationModal.svelte +100 -100
- package/src/ruvocal/src/lib/components/ExpandNavigation.svelte +22 -22
- package/src/ruvocal/src/lib/components/FoundationBackground.svelte +242 -242
- package/src/ruvocal/src/lib/components/HoverTooltip.svelte +44 -44
- package/src/ruvocal/src/lib/components/HtmlPreviewModal.svelte +143 -143
- package/src/ruvocal/src/lib/components/InfiniteScroll.svelte +50 -50
- package/src/ruvocal/src/lib/components/MobileNav.svelte +300 -300
- package/src/ruvocal/src/lib/components/Modal.svelte +115 -115
- package/src/ruvocal/src/lib/components/ModelCardMetadata.svelte +71 -71
- package/src/ruvocal/src/lib/components/NavConversationItem.svelte +151 -151
- package/src/ruvocal/src/lib/components/NavMenu.svelte +313 -313
- package/src/ruvocal/src/lib/components/Pagination.svelte +97 -97
- package/src/ruvocal/src/lib/components/PaginationArrow.svelte +27 -27
- package/src/ruvocal/src/lib/components/Portal.svelte +24 -24
- package/src/ruvocal/src/lib/components/RetryBtn.svelte +18 -18
- package/src/ruvocal/src/lib/components/RuFloUniverse.svelte +185 -185
- package/src/ruvocal/src/lib/components/RufloHelpModal.svelte +411 -411
- package/src/ruvocal/src/lib/components/ScrollToBottomBtn.svelte +47 -47
- package/src/ruvocal/src/lib/components/ScrollToPreviousBtn.svelte +77 -77
- package/src/ruvocal/src/lib/components/ShareConversationModal.svelte +182 -182
- package/src/ruvocal/src/lib/components/StopGeneratingBtn.svelte +69 -69
- package/src/ruvocal/src/lib/components/SubscribeModal.svelte +87 -87
- package/src/ruvocal/src/lib/components/Switch.svelte +36 -36
- package/src/ruvocal/src/lib/components/SystemPromptModal.svelte +44 -44
- package/src/ruvocal/src/lib/components/Toast.svelte +27 -27
- package/src/ruvocal/src/lib/components/Tooltip.svelte +30 -30
- package/src/ruvocal/src/lib/components/WelcomeModal.svelte +46 -46
- package/src/ruvocal/src/lib/components/chat/Alternatives.svelte +77 -77
- package/src/ruvocal/src/lib/components/chat/BlockWrapper.svelte +72 -72
- package/src/ruvocal/src/lib/components/chat/ChatInput.svelte +490 -490
- package/src/ruvocal/src/lib/components/chat/ChatIntroduction.svelte +123 -123
- package/src/ruvocal/src/lib/components/chat/ChatMessage.svelte +548 -548
- package/src/ruvocal/src/lib/components/chat/ChatWindow.svelte +1057 -1057
- package/src/ruvocal/src/lib/components/chat/FileDropzone.svelte +92 -92
- package/src/ruvocal/src/lib/components/chat/ImageLightbox.svelte +66 -66
- package/src/ruvocal/src/lib/components/chat/MarkdownBlock.svelte +23 -23
- package/src/ruvocal/src/lib/components/chat/MarkdownRenderer.svelte +69 -69
- package/src/ruvocal/src/lib/components/chat/MarkdownRenderer.svelte.test.ts +58 -58
- package/src/ruvocal/src/lib/components/chat/MessageAvatar.svelte +103 -103
- package/src/ruvocal/src/lib/components/chat/ModelSwitch.svelte +64 -64
- package/src/ruvocal/src/lib/components/chat/OpenReasoningResults.svelte +81 -81
- package/src/ruvocal/src/lib/components/chat/TaskGroup.svelte +88 -88
- package/src/ruvocal/src/lib/components/chat/ToolUpdate.svelte +273 -273
- package/src/ruvocal/src/lib/components/chat/UploadedFile.svelte +253 -253
- package/src/ruvocal/src/lib/components/chat/UrlFetchModal.svelte +203 -203
- package/src/ruvocal/src/lib/components/chat/VoiceRecorder.svelte +214 -214
- package/src/ruvocal/src/lib/components/icons/IconBurger.svelte +20 -20
- package/src/ruvocal/src/lib/components/icons/IconCheap.svelte +20 -20
- package/src/ruvocal/src/lib/components/icons/IconChevron.svelte +24 -24
- package/src/ruvocal/src/lib/components/icons/IconDazzled.svelte +40 -40
- package/src/ruvocal/src/lib/components/icons/IconFast.svelte +20 -20
- package/src/ruvocal/src/lib/components/icons/IconLoading.svelte +22 -22
- package/src/ruvocal/src/lib/components/icons/IconMCP.svelte +28 -28
- package/src/ruvocal/src/lib/components/icons/IconMoon.svelte +21 -21
- package/src/ruvocal/src/lib/components/icons/IconNew.svelte +20 -20
- package/src/ruvocal/src/lib/components/icons/IconOmni.svelte +90 -90
- package/src/ruvocal/src/lib/components/icons/IconPaperclip.svelte +24 -24
- package/src/ruvocal/src/lib/components/icons/IconPro.svelte +37 -37
- package/src/ruvocal/src/lib/components/icons/IconShare.svelte +21 -21
- package/src/ruvocal/src/lib/components/icons/IconSun.svelte +93 -93
- package/src/ruvocal/src/lib/components/icons/Logo.svelte +68 -68
- package/src/ruvocal/src/lib/components/icons/LogoHuggingFaceBorderless.svelte +54 -54
- package/src/ruvocal/src/lib/components/mcp/AddServerForm.svelte +250 -250
- package/src/ruvocal/src/lib/components/mcp/MCPServerManager.svelte +185 -185
- package/src/ruvocal/src/lib/components/mcp/ServerCard.svelte +203 -203
- package/src/ruvocal/src/lib/components/players/AudioPlayer.svelte +82 -82
- package/src/ruvocal/src/lib/components/voice/AudioWaveform.svelte +96 -96
- package/src/ruvocal/src/lib/components/wasm/GalleryPanel.svelte +357 -357
- package/src/ruvocal/src/lib/constants/mcpExamples.ts +114 -114
- package/src/ruvocal/src/lib/constants/mime.ts +11 -11
- package/src/ruvocal/src/lib/constants/pagination.ts +1 -1
- package/src/ruvocal/src/lib/constants/publicSepToken.ts +1 -1
- package/src/ruvocal/src/lib/constants/routerExamples.ts +133 -133
- package/src/ruvocal/src/lib/constants/rvagentPresets.ts +206 -206
- package/src/ruvocal/src/lib/createShareLink.ts +27 -27
- package/src/ruvocal/src/lib/jobs/refresh-conversation-stats.ts +297 -297
- package/src/ruvocal/src/lib/migrations/lock.ts +56 -56
- package/src/ruvocal/src/lib/migrations/migrations.spec.ts +74 -74
- package/src/ruvocal/src/lib/migrations/migrations.ts +109 -109
- package/src/ruvocal/src/lib/migrations/routines/01-update-search-assistants.ts +50 -50
- package/src/ruvocal/src/lib/migrations/routines/02-update-assistants-models.ts +48 -48
- package/src/ruvocal/src/lib/migrations/routines/04-update-message-updates.ts +151 -151
- package/src/ruvocal/src/lib/migrations/routines/05-update-message-files.ts +56 -56
- package/src/ruvocal/src/lib/migrations/routines/06-trim-message-updates.ts +56 -56
- package/src/ruvocal/src/lib/migrations/routines/08-update-featured-to-review.ts +32 -32
- package/src/ruvocal/src/lib/migrations/routines/09-delete-empty-conversations.spec.ts +214 -214
- package/src/ruvocal/src/lib/migrations/routines/09-delete-empty-conversations.ts +88 -88
- package/src/ruvocal/src/lib/migrations/routines/10-update-reports-assistantid.ts +29 -29
- package/src/ruvocal/src/lib/migrations/routines/index.ts +15 -15
- package/src/ruvocal/src/lib/server/__tests__/conversation-stop-generating.spec.ts +103 -103
- package/src/ruvocal/src/lib/server/abortRegistry.ts +57 -57
- package/src/ruvocal/src/lib/server/abortedGenerations.ts +43 -43
- package/src/ruvocal/src/lib/server/adminToken.ts +62 -62
- package/src/ruvocal/src/lib/server/api/__tests__/conversations-id.spec.ts +296 -296
- package/src/ruvocal/src/lib/server/api/__tests__/conversations-message.spec.ts +216 -216
- package/src/ruvocal/src/lib/server/api/__tests__/conversations.spec.ts +235 -235
- package/src/ruvocal/src/lib/server/api/__tests__/misc.spec.ts +72 -72
- package/src/ruvocal/src/lib/server/api/__tests__/testHelpers.ts +86 -86
- package/src/ruvocal/src/lib/server/api/__tests__/user-reports.spec.ts +78 -78
- package/src/ruvocal/src/lib/server/api/__tests__/user.spec.ts +239 -239
- package/src/ruvocal/src/lib/server/api/types.ts +37 -37
- package/src/ruvocal/src/lib/server/api/utils/requireAuth.ts +22 -22
- package/src/ruvocal/src/lib/server/api/utils/resolveConversation.ts +69 -69
- package/src/ruvocal/src/lib/server/api/utils/resolveModel.ts +27 -27
- package/src/ruvocal/src/lib/server/api/utils/superjsonResponse.ts +15 -15
- package/src/ruvocal/src/lib/server/apiToken.ts +11 -11
- package/src/ruvocal/src/lib/server/auth.ts +554 -554
- package/src/ruvocal/src/lib/server/config.ts +187 -187
- package/src/ruvocal/src/lib/server/conversation.ts +83 -83
- package/src/ruvocal/src/lib/server/database/__tests__/rvf.spec.ts +709 -709
- package/src/ruvocal/src/lib/server/database/postgres.ts +700 -700
- package/src/ruvocal/src/lib/server/database/rvf.ts +1078 -1078
- package/src/ruvocal/src/lib/server/database.ts +145 -145
- package/src/ruvocal/src/lib/server/endpoints/document.ts +68 -68
- package/src/ruvocal/src/lib/server/endpoints/endpoints.ts +43 -43
- package/src/ruvocal/src/lib/server/endpoints/images.ts +211 -211
- package/src/ruvocal/src/lib/server/endpoints/openai/endpointOai.ts +266 -266
- package/src/ruvocal/src/lib/server/endpoints/openai/openAIChatToTextGenerationStream.ts +212 -212
- package/src/ruvocal/src/lib/server/endpoints/openai/openAICompletionToTextGenerationStream.ts +32 -32
- package/src/ruvocal/src/lib/server/endpoints/preprocessMessages.ts +61 -61
- package/src/ruvocal/src/lib/server/exitHandler.ts +59 -59
- package/src/ruvocal/src/lib/server/files/downloadFile.ts +34 -34
- package/src/ruvocal/src/lib/server/files/uploadFile.ts +29 -29
- package/src/ruvocal/src/lib/server/findRepoRoot.ts +13 -13
- package/src/ruvocal/src/lib/server/generateFromDefaultEndpoint.ts +46 -46
- package/src/ruvocal/src/lib/server/hooks/error.ts +37 -37
- package/src/ruvocal/src/lib/server/hooks/fetch.ts +22 -22
- package/src/ruvocal/src/lib/server/hooks/handle.ts +250 -250
- package/src/ruvocal/src/lib/server/hooks/init.ts +51 -51
- package/src/ruvocal/src/lib/server/isURLLocal.spec.ts +31 -31
- package/src/ruvocal/src/lib/server/isURLLocal.ts +74 -74
- package/src/ruvocal/src/lib/server/logger.ts +42 -42
- package/src/ruvocal/src/lib/server/mcp/clientPool.spec.ts +175 -175
- package/src/ruvocal/src/lib/server/mcp/hf.ts +32 -32
- package/src/ruvocal/src/lib/server/mcp/httpClient.ts +122 -122
- package/src/ruvocal/src/lib/server/mcp/registry.ts +76 -76
- package/src/ruvocal/src/lib/server/mcp/tools.ts +196 -196
- package/src/ruvocal/src/lib/server/metrics.ts +255 -255
- package/src/ruvocal/src/lib/server/models.ts +518 -518
- package/src/ruvocal/src/lib/server/requestContext.ts +55 -55
- package/src/ruvocal/src/lib/server/router/arch.ts +230 -230
- package/src/ruvocal/src/lib/server/router/endpoint.ts +316 -316
- package/src/ruvocal/src/lib/server/router/multimodal.ts +28 -28
- package/src/ruvocal/src/lib/server/router/policy.ts +49 -49
- package/src/ruvocal/src/lib/server/router/toolsRoute.ts +51 -51
- package/src/ruvocal/src/lib/server/router/types.ts +21 -21
- package/src/ruvocal/src/lib/server/sendSlack.ts +23 -23
- package/src/ruvocal/src/lib/server/textGeneration/generate.ts +258 -258
- package/src/ruvocal/src/lib/server/textGeneration/index.ts +96 -96
- package/src/ruvocal/src/lib/server/textGeneration/mcp/fileRefs.ts +155 -155
- package/src/ruvocal/src/lib/server/textGeneration/mcp/routerResolution.ts +108 -108
- package/src/ruvocal/src/lib/server/textGeneration/mcp/runMcpFlow.ts +831 -831
- package/src/ruvocal/src/lib/server/textGeneration/mcp/toolInvocation.ts +349 -349
- package/src/ruvocal/src/lib/server/textGeneration/mcp/wasmTools.test.ts +633 -633
- package/src/ruvocal/src/lib/server/textGeneration/reasoning.ts +23 -23
- package/src/ruvocal/src/lib/server/textGeneration/title.ts +83 -83
- package/src/ruvocal/src/lib/server/textGeneration/types.ts +28 -28
- package/src/ruvocal/src/lib/server/textGeneration/utils/prepareFiles.ts +88 -88
- package/src/ruvocal/src/lib/server/textGeneration/utils/routing.ts +21 -21
- package/src/ruvocal/src/lib/server/textGeneration/utils/toolPrompt.ts +49 -49
- package/src/ruvocal/src/lib/server/urlSafety.ts +77 -77
- package/src/ruvocal/src/lib/server/usageLimits.ts +30 -30
- package/src/ruvocal/src/lib/stores/autopilotStore.svelte.ts +175 -175
- package/src/ruvocal/src/lib/stores/backgroundGenerations.svelte.ts +32 -32
- package/src/ruvocal/src/lib/stores/backgroundGenerations.ts +1 -1
- package/src/ruvocal/src/lib/stores/errors.ts +9 -9
- package/src/ruvocal/src/lib/stores/isAborted.ts +3 -3
- package/src/ruvocal/src/lib/stores/isPro.ts +4 -4
- package/src/ruvocal/src/lib/stores/loading.ts +3 -3
- package/src/ruvocal/src/lib/stores/mcpServers.ts +534 -534
- package/src/ruvocal/src/lib/stores/pendingChatInput.ts +3 -3
- package/src/ruvocal/src/lib/stores/pendingMessage.ts +9 -9
- package/src/ruvocal/src/lib/stores/settings.ts +182 -182
- package/src/ruvocal/src/lib/stores/shareModal.ts +13 -13
- package/src/ruvocal/src/lib/stores/titleUpdate.ts +8 -8
- package/src/ruvocal/src/lib/stores/wasmMcp.ts +472 -472
- package/src/ruvocal/src/lib/switchTheme.ts +124 -124
- package/src/ruvocal/src/lib/types/AbortedGeneration.ts +8 -8
- package/src/ruvocal/src/lib/types/Assistant.ts +31 -31
- package/src/ruvocal/src/lib/types/AssistantStats.ts +11 -11
- package/src/ruvocal/src/lib/types/ConfigKey.ts +4 -4
- package/src/ruvocal/src/lib/types/ConvSidebar.ts +9 -9
- package/src/ruvocal/src/lib/types/Conversation.ts +27 -27
- package/src/ruvocal/src/lib/types/ConversationStats.ts +13 -13
- package/src/ruvocal/src/lib/types/Message.ts +41 -41
- package/src/ruvocal/src/lib/types/MessageEvent.ts +10 -10
- package/src/ruvocal/src/lib/types/MessageUpdate.ts +139 -139
- package/src/ruvocal/src/lib/types/MigrationResult.ts +7 -7
- package/src/ruvocal/src/lib/types/Model.ts +23 -23
- package/src/ruvocal/src/lib/types/Report.ts +12 -12
- package/src/ruvocal/src/lib/types/Review.ts +6 -6
- package/src/ruvocal/src/lib/types/Semaphore.ts +19 -19
- package/src/ruvocal/src/lib/types/Session.ts +22 -22
- package/src/ruvocal/src/lib/types/Settings.ts +93 -93
- package/src/ruvocal/src/lib/types/SharedConversation.ts +9 -9
- package/src/ruvocal/src/lib/types/Template.ts +6 -6
- package/src/ruvocal/src/lib/types/Timestamps.ts +4 -4
- package/src/ruvocal/src/lib/types/TokenCache.ts +6 -6
- package/src/ruvocal/src/lib/types/Tool.ts +77 -77
- package/src/ruvocal/src/lib/types/UrlDependency.ts +5 -5
- package/src/ruvocal/src/lib/types/User.ts +14 -14
- package/src/ruvocal/src/lib/utils/PublicConfig.svelte.ts +75 -75
- package/src/ruvocal/src/lib/utils/auth.ts +17 -17
- package/src/ruvocal/src/lib/utils/chunk.ts +33 -33
- package/src/ruvocal/src/lib/utils/cookiesAreEnabled.ts +13 -13
- package/src/ruvocal/src/lib/utils/debounce.ts +17 -17
- package/src/ruvocal/src/lib/utils/deepestChild.ts +6 -6
- package/src/ruvocal/src/lib/utils/favicon.ts +21 -21
- package/src/ruvocal/src/lib/utils/fetchJSON.ts +23 -23
- package/src/ruvocal/src/lib/utils/file2base64.ts +14 -14
- package/src/ruvocal/src/lib/utils/formatUserCount.ts +37 -37
- package/src/ruvocal/src/lib/utils/generationState.spec.ts +75 -75
- package/src/ruvocal/src/lib/utils/generationState.ts +26 -26
- package/src/ruvocal/src/lib/utils/getHref.ts +41 -41
- package/src/ruvocal/src/lib/utils/getReturnFromGenerator.ts +7 -7
- package/src/ruvocal/src/lib/utils/haptics.ts +64 -64
- package/src/ruvocal/src/lib/utils/hashConv.ts +12 -12
- package/src/ruvocal/src/lib/utils/hf.ts +17 -17
- package/src/ruvocal/src/lib/utils/isDesktop.ts +7 -7
- package/src/ruvocal/src/lib/utils/isUrl.ts +8 -8
- package/src/ruvocal/src/lib/utils/isVirtualKeyboard.ts +16 -16
- package/src/ruvocal/src/lib/utils/loadAttachmentsFromUrls.ts +115 -115
- package/src/ruvocal/src/lib/utils/marked.spec.ts +96 -96
- package/src/ruvocal/src/lib/utils/marked.ts +531 -531
- package/src/ruvocal/src/lib/utils/mcpValidation.ts +147 -147
- package/src/ruvocal/src/lib/utils/mergeAsyncGenerators.ts +38 -38
- package/src/ruvocal/src/lib/utils/messageUpdates.spec.ts +262 -262
- package/src/ruvocal/src/lib/utils/messageUpdates.ts +324 -324
- package/src/ruvocal/src/lib/utils/mime.ts +56 -56
- package/src/ruvocal/src/lib/utils/models.ts +14 -14
- package/src/ruvocal/src/lib/utils/parseBlocks.ts +120 -120
- package/src/ruvocal/src/lib/utils/parseIncompleteMarkdown.ts +644 -644
- package/src/ruvocal/src/lib/utils/parseStringToList.ts +10 -10
- package/src/ruvocal/src/lib/utils/randomUuid.ts +14 -14
- package/src/ruvocal/src/lib/utils/searchTokens.ts +33 -33
- package/src/ruvocal/src/lib/utils/sha256.ts +7 -7
- package/src/ruvocal/src/lib/utils/stringifyError.ts +12 -12
- package/src/ruvocal/src/lib/utils/sum.ts +3 -3
- package/src/ruvocal/src/lib/utils/template.spec.ts +59 -59
- package/src/ruvocal/src/lib/utils/template.ts +53 -53
- package/src/ruvocal/src/lib/utils/timeout.ts +9 -9
- package/src/ruvocal/src/lib/utils/toolProgress.spec.ts +46 -46
- package/src/ruvocal/src/lib/utils/toolProgress.ts +11 -11
- package/src/ruvocal/src/lib/utils/tree/addChildren.spec.ts +102 -102
- package/src/ruvocal/src/lib/utils/tree/addChildren.ts +48 -48
- package/src/ruvocal/src/lib/utils/tree/addSibling.spec.ts +81 -81
- package/src/ruvocal/src/lib/utils/tree/addSibling.ts +41 -41
- package/src/ruvocal/src/lib/utils/tree/buildSubtree.spec.ts +110 -110
- package/src/ruvocal/src/lib/utils/tree/buildSubtree.ts +24 -24
- package/src/ruvocal/src/lib/utils/tree/convertLegacyConversation.spec.ts +31 -31
- package/src/ruvocal/src/lib/utils/tree/convertLegacyConversation.ts +36 -36
- package/src/ruvocal/src/lib/utils/tree/isMessageId.spec.ts +15 -15
- package/src/ruvocal/src/lib/utils/tree/isMessageId.ts +5 -5
- package/src/ruvocal/src/lib/utils/tree/tree.d.ts +14 -14
- package/src/ruvocal/src/lib/utils/tree/treeHelpers.spec.ts +167 -167
- package/src/ruvocal/src/lib/utils/updates.ts +39 -39
- package/src/ruvocal/src/lib/utils/urlParams.ts +13 -13
- package/src/ruvocal/src/lib/wasm/idb.ts +438 -438
- package/src/ruvocal/src/lib/wasm/index.ts +1213 -1213
- package/src/ruvocal/src/lib/wasm/tests/wasm-capabilities.test.ts +565 -565
- package/src/ruvocal/src/lib/wasm/wasm.worker.ts +332 -332
- package/src/ruvocal/src/lib/wasm/workerClient.ts +166 -166
- package/src/ruvocal/src/lib/workers/autopilotWorker.ts +221 -221
- package/src/ruvocal/src/lib/workers/detailFetchWorker.ts +100 -100
- package/src/ruvocal/src/lib/workers/markdownWorker.ts +61 -61
- package/src/ruvocal/src/routes/+error.svelte +20 -20
- package/src/ruvocal/src/routes/+layout.svelte +324 -324
- package/src/ruvocal/src/routes/+layout.ts +91 -91
- package/src/ruvocal/src/routes/+page.svelte +168 -168
- package/src/ruvocal/src/routes/.well-known/oauth-cimd/+server.ts +37 -37
- package/src/ruvocal/src/routes/__debug/openai/+server.ts +21 -21
- package/src/ruvocal/src/routes/admin/export/+server.ts +159 -159
- package/src/ruvocal/src/routes/admin/stats/compute/+server.ts +16 -16
- package/src/ruvocal/src/routes/api/conversation/[id]/+server.ts +40 -40
- package/src/ruvocal/src/routes/api/conversation/[id]/message/[messageId]/+server.ts +42 -42
- package/src/ruvocal/src/routes/api/conversations/+server.ts +48 -48
- package/src/ruvocal/src/routes/api/fetch-url/+server.ts +147 -147
- package/src/ruvocal/src/routes/api/mcp/health/+server.ts +292 -292
- package/src/ruvocal/src/routes/api/mcp/servers/+server.ts +32 -32
- package/src/ruvocal/src/routes/api/models/+server.ts +25 -25
- package/src/ruvocal/src/routes/api/transcribe/+server.ts +104 -104
- package/src/ruvocal/src/routes/api/user/+server.ts +15 -15
- package/src/ruvocal/src/routes/api/user/validate-token/+server.ts +20 -20
- package/src/ruvocal/src/routes/api/v2/conversations/+server.ts +48 -48
- package/src/ruvocal/src/routes/api/v2/conversations/[id]/+server.ts +94 -94
- package/src/ruvocal/src/routes/api/v2/conversations/[id]/message/[messageId]/+server.ts +43 -43
- package/src/ruvocal/src/routes/api/v2/conversations/import-share/+server.ts +23 -23
- package/src/ruvocal/src/routes/api/v2/debug/config/+server.ts +16 -16
- package/src/ruvocal/src/routes/api/v2/debug/refresh/+server.ts +30 -30
- package/src/ruvocal/src/routes/api/v2/export/+server.ts +196 -196
- package/src/ruvocal/src/routes/api/v2/feature-flags/+server.ts +14 -14
- package/src/ruvocal/src/routes/api/v2/models/+server.ts +38 -38
- package/src/ruvocal/src/routes/api/v2/models/[namespace]/+server.ts +8 -8
- package/src/ruvocal/src/routes/api/v2/models/[namespace]/[model]/+server.ts +8 -8
- package/src/ruvocal/src/routes/api/v2/models/[namespace]/[model]/subscribe/+server.ts +28 -28
- package/src/ruvocal/src/routes/api/v2/models/[namespace]/subscribe/+server.ts +28 -28
- package/src/ruvocal/src/routes/api/v2/models/old/+server.ts +7 -7
- package/src/ruvocal/src/routes/api/v2/models/refresh/+server.ts +33 -33
- package/src/ruvocal/src/routes/api/v2/public-config/+server.ts +7 -7
- package/src/ruvocal/src/routes/api/v2/user/+server.ts +17 -17
- package/src/ruvocal/src/routes/api/v2/user/billing-orgs/+server.ts +73 -73
- package/src/ruvocal/src/routes/api/v2/user/reports/+server.ts +17 -17
- package/src/ruvocal/src/routes/api/v2/user/settings/+server.ts +110 -110
- package/src/ruvocal/src/routes/conversation/+server.ts +115 -115
- package/src/ruvocal/src/routes/conversation/[id]/+page.svelte +586 -586
- package/src/ruvocal/src/routes/conversation/[id]/+page.ts +60 -60
- package/src/ruvocal/src/routes/conversation/[id]/+server.ts +740 -740
- package/src/ruvocal/src/routes/conversation/[id]/message/[messageId]/prompt/+server.ts +66 -66
- package/src/ruvocal/src/routes/conversation/[id]/share/+server.ts +69 -69
- package/src/ruvocal/src/routes/conversation/[id]/stop-generating/+server.ts +35 -35
- package/src/ruvocal/src/routes/healthcheck/+server.ts +3 -3
- package/src/ruvocal/src/routes/login/+server.ts +5 -5
- package/src/ruvocal/src/routes/login/callback/+server.ts +103 -103
- package/src/ruvocal/src/routes/login/callback/updateUser.spec.ts +157 -157
- package/src/ruvocal/src/routes/login/callback/updateUser.ts +215 -215
- package/src/ruvocal/src/routes/logout/+server.ts +18 -18
- package/src/ruvocal/src/routes/metrics/+server.ts +18 -18
- package/src/ruvocal/src/routes/models/+page.svelte +233 -233
- package/src/ruvocal/src/routes/models/[...model]/+page.svelte +161 -161
- package/src/ruvocal/src/routes/models/[...model]/+page.ts +14 -14
- package/src/ruvocal/src/routes/models/[...model]/thumbnail.png/+server.ts +64 -64
- package/src/ruvocal/src/routes/models/[...model]/thumbnail.png/ModelThumbnail.svelte +28 -28
- package/src/ruvocal/src/routes/privacy/+page.svelte +11 -11
- package/src/ruvocal/src/routes/r/[id]/+page.ts +34 -34
- package/src/ruvocal/src/routes/settings/(nav)/+layout.svelte +282 -282
- package/src/ruvocal/src/routes/settings/(nav)/+layout.ts +1 -1
- package/src/ruvocal/src/routes/settings/(nav)/+server.ts +59 -59
- package/src/ruvocal/src/routes/settings/(nav)/[...model]/+page.svelte +464 -464
- package/src/ruvocal/src/routes/settings/(nav)/[...model]/+page.ts +14 -14
- package/src/ruvocal/src/routes/settings/(nav)/application/+page.svelte +362 -362
- package/src/ruvocal/src/routes/settings/+layout.svelte +40 -40
- package/src/ruvocal/src/styles/highlight-js.css +195 -195
- package/src/ruvocal/src/styles/main.css +144 -144
- package/src/ruvocal/static/chatui/favicon-dark.svg +3 -3
- package/src/ruvocal/static/chatui/favicon-dev.svg +3 -3
- package/src/ruvocal/static/chatui/favicon.svg +3 -3
- package/src/ruvocal/static/chatui/icon.svg +3 -3
- package/src/ruvocal/static/chatui/logo.svg +7 -7
- package/src/ruvocal/static/chatui/manifest.json +54 -54
- package/src/ruvocal/static/chatui/welcome.js +184 -184
- package/src/ruvocal/static/huggingchat/favicon-dark.svg +4 -4
- package/src/ruvocal/static/huggingchat/favicon-dev.svg +4 -4
- package/src/ruvocal/static/huggingchat/favicon.svg +4 -4
- package/src/ruvocal/static/huggingchat/fulltext-logo.svg +1 -1
- package/src/ruvocal/static/huggingchat/icon.svg +4 -4
- package/src/ruvocal/static/huggingchat/logo.svg +4 -4
- package/src/ruvocal/static/huggingchat/manifest.json +54 -54
- package/src/ruvocal/static/huggingchat/routes.chat.json +226 -226
- package/src/ruvocal/static/robots.txt +10 -10
- package/src/ruvocal/static/wasm/rvagent_wasm.js +1539 -1539
- package/src/ruvocal/stub/@reflink/reflink/package.json +5 -5
- package/src/ruvocal/svelte.config.js +53 -53
- package/src/ruvocal/tailwind.config.cjs +30 -30
- package/src/ruvocal/tsconfig.json +19 -19
- package/src/ruvocal/vite.config.ts +87 -87
- package/src/scripts/deploy.sh +116 -116
- package/src/scripts/generate-config.js +245 -245
- package/src/scripts/generate-welcome.js +187 -187
- package/src/scripts/package-rvf.sh +116 -116
- package/src/ruvocal/.claude-flow/daemon-state.json +0 -135
- package/src/ruvocal/.claude-flow/data/pending-insights.jsonl +0 -0
- package/src/ruvocal/.claude-flow/data/ranked-context.json +0 -5
- package/src/ruvocal/.claude-flow/logs/daemon.log +0 -31
- package/src/ruvocal/.claude-flow/logs/headless/audit_1777949411822_juxau0_prompt.log +0 -989
- package/src/ruvocal/.claude-flow/logs/headless/audit_1777949411822_juxau0_result.log +0 -67
- package/src/ruvocal/.claude-flow/logs/headless/audit_1777950042278_jvj5xq_prompt.log +0 -989
- package/src/ruvocal/.claude-flow/logs/headless/audit_1777950042278_jvj5xq_result.log +0 -93
- package/src/ruvocal/.claude-flow/logs/headless/optimize_1777949531823_yt5yc2_prompt.log +0 -1498
- package/src/ruvocal/.claude-flow/logs/headless/optimize_1777949531823_yt5yc2_result.log +0 -93
- package/src/ruvocal/.claude-flow/logs/headless/testgaps_1777949771821_elw1j4_prompt.log +0 -1498
- package/src/ruvocal/.claude-flow/logs/headless/testgaps_1777949771821_elw1j4_result.log +0 -100
- package/src/ruvocal/.claude-flow/metrics/codebase-map.json +0 -11
- package/src/ruvocal/.claude-flow/metrics/consolidation.json +0 -6
- package/src/ruvocal/.claude-flow/neural/stats.json +0 -6
- package/src/ruvocal/.claude-flow/sessions/current.json +0 -13
- package/src/ruvocal/.swarm/attestation.db +0 -0
- package/src/ruvocal/.swarm/hnsw.index +0 -0
- package/src/ruvocal/.swarm/hnsw.metadata.json +0 -1
- package/src/ruvocal/.swarm/memory.db +0 -0
- package/src/ruvocal/.swarm/schema.sql +0 -305
|
@@ -1,548 +1,548 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import type { Message } from "$lib/types/Message";
|
|
3
|
-
import { tick } from "svelte";
|
|
4
|
-
|
|
5
|
-
import { usePublicConfig } from "$lib/utils/PublicConfig.svelte";
|
|
6
|
-
const publicConfig = usePublicConfig();
|
|
7
|
-
import CopyToClipBoardBtn from "../CopyToClipBoardBtn.svelte";
|
|
8
|
-
import IconLoading from "../icons/IconLoading.svelte";
|
|
9
|
-
import CarbonRotate360 from "~icons/carbon/rotate-360";
|
|
10
|
-
// import CarbonDownload from "~icons/carbon/download";
|
|
11
|
-
|
|
12
|
-
import CarbonPen from "~icons/carbon/pen";
|
|
13
|
-
import UploadedFile from "./UploadedFile.svelte";
|
|
14
|
-
|
|
15
|
-
import MarkdownRenderer from "./MarkdownRenderer.svelte";
|
|
16
|
-
import OpenReasoningResults from "./OpenReasoningResults.svelte";
|
|
17
|
-
import Alternatives from "./Alternatives.svelte";
|
|
18
|
-
import MessageAvatar from "./MessageAvatar.svelte";
|
|
19
|
-
import { PROVIDERS_HUB_ORGS } from "@huggingface/inference";
|
|
20
|
-
import { requireAuthUser } from "$lib/utils/auth";
|
|
21
|
-
import ToolUpdate from "./ToolUpdate.svelte";
|
|
22
|
-
import TaskGroup from "./TaskGroup.svelte";
|
|
23
|
-
import { isMessageToolUpdate } from "$lib/utils/messageUpdates";
|
|
24
|
-
import { MessageUpdateType, type MessageToolUpdate } from "$lib/types/MessageUpdate";
|
|
25
|
-
import ImageLightbox from "./ImageLightbox.svelte";
|
|
26
|
-
|
|
27
|
-
interface Props {
|
|
28
|
-
message: Message;
|
|
29
|
-
loading?: boolean;
|
|
30
|
-
isAuthor?: boolean;
|
|
31
|
-
readOnly?: boolean;
|
|
32
|
-
isTapped?: boolean;
|
|
33
|
-
alternatives?: Message["id"][];
|
|
34
|
-
editMsdgId?: Message["id"] | null;
|
|
35
|
-
isLast?: boolean;
|
|
36
|
-
onretry?: (payload: { id: Message["id"]; content?: string }) => void;
|
|
37
|
-
onshowAlternateMsg?: (payload: { id: Message["id"] }) => void;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
let {
|
|
41
|
-
message,
|
|
42
|
-
loading = false,
|
|
43
|
-
isAuthor: _isAuthor = true,
|
|
44
|
-
readOnly: _readOnly = false,
|
|
45
|
-
isTapped = $bindable(false),
|
|
46
|
-
alternatives = [],
|
|
47
|
-
editMsdgId = $bindable(null),
|
|
48
|
-
isLast = false,
|
|
49
|
-
onretry,
|
|
50
|
-
onshowAlternateMsg,
|
|
51
|
-
}: Props = $props();
|
|
52
|
-
|
|
53
|
-
let contentEl: HTMLElement | undefined = $state();
|
|
54
|
-
let isCopied = $state(false);
|
|
55
|
-
let messageWidth: number = $state(0);
|
|
56
|
-
let messageInfoWidth: number = $state(0);
|
|
57
|
-
let lightboxSrc: string | null = $state(null);
|
|
58
|
-
|
|
59
|
-
function handleContentClick(e: MouseEvent) {
|
|
60
|
-
const target = e.target as HTMLElement;
|
|
61
|
-
if (target.tagName === "IMG" && target instanceof HTMLImageElement) {
|
|
62
|
-
e.preventDefault();
|
|
63
|
-
e.stopPropagation();
|
|
64
|
-
lightboxSrc = target.src;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
$effect(() => {
|
|
69
|
-
// referenced to appease linter for currently-unused props
|
|
70
|
-
void _isAuthor;
|
|
71
|
-
void _readOnly;
|
|
72
|
-
});
|
|
73
|
-
function handleKeyDown(e: KeyboardEvent) {
|
|
74
|
-
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
|
|
75
|
-
editFormEl?.requestSubmit();
|
|
76
|
-
}
|
|
77
|
-
if (e.key === "Escape") {
|
|
78
|
-
editMsdgId = null;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function handleCopy(event: ClipboardEvent) {
|
|
83
|
-
if (!contentEl) return;
|
|
84
|
-
|
|
85
|
-
const selection = window.getSelection();
|
|
86
|
-
if (!selection || selection.isCollapsed) return;
|
|
87
|
-
if (!selection.anchorNode || !selection.focusNode) return;
|
|
88
|
-
|
|
89
|
-
const anchorInside = contentEl.contains(selection.anchorNode);
|
|
90
|
-
const focusInside = contentEl.contains(selection.focusNode);
|
|
91
|
-
if (!anchorInside && !focusInside) return;
|
|
92
|
-
|
|
93
|
-
if (!event.clipboardData) return;
|
|
94
|
-
|
|
95
|
-
const range = selection.getRangeAt(0);
|
|
96
|
-
const wrapper = document.createElement("div");
|
|
97
|
-
wrapper.appendChild(range.cloneContents());
|
|
98
|
-
|
|
99
|
-
wrapper.querySelectorAll("[data-exclude-from-copy]").forEach((el) => {
|
|
100
|
-
el.remove();
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
wrapper.querySelectorAll("*").forEach((el) => {
|
|
104
|
-
el.removeAttribute("style");
|
|
105
|
-
el.removeAttribute("class");
|
|
106
|
-
el.removeAttribute("color");
|
|
107
|
-
el.removeAttribute("bgcolor");
|
|
108
|
-
el.removeAttribute("background");
|
|
109
|
-
|
|
110
|
-
for (const attr of Array.from(el.attributes)) {
|
|
111
|
-
if (attr.name === "id" || attr.name.startsWith("data-")) {
|
|
112
|
-
el.removeAttribute(attr.name);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
const html = wrapper.innerHTML;
|
|
118
|
-
const text = wrapper.textContent ?? "";
|
|
119
|
-
|
|
120
|
-
event.preventDefault();
|
|
121
|
-
event.clipboardData.setData("text/html", html);
|
|
122
|
-
event.clipboardData.setData("text/plain", text);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
let editContentEl: HTMLTextAreaElement | undefined = $state();
|
|
126
|
-
let editFormEl: HTMLFormElement | undefined = $state();
|
|
127
|
-
|
|
128
|
-
// Zero-config reasoning autodetection: detect <think> blocks in content
|
|
129
|
-
const THINK_BLOCK_REGEX = /(<think>[\s\S]*?(?:<\/think>|$))/gi;
|
|
130
|
-
// Non-global version for .test() calls to avoid lastIndex side effects
|
|
131
|
-
const THINK_BLOCK_TEST_REGEX = /(<think>[\s\S]*?(?:<\/think>|$))/i;
|
|
132
|
-
let hasClientThink = $derived(message.content.split(THINK_BLOCK_REGEX).length > 1);
|
|
133
|
-
|
|
134
|
-
// Strip think blocks for clipboard copy (always, regardless of detection)
|
|
135
|
-
let contentWithoutThink = $derived.by(() =>
|
|
136
|
-
message.content.replace(THINK_BLOCK_REGEX, "").trim()
|
|
137
|
-
);
|
|
138
|
-
|
|
139
|
-
type Block =
|
|
140
|
-
| { type: "text"; content: string }
|
|
141
|
-
| { type: "tool"; uuid: string; updates: MessageToolUpdate[] }
|
|
142
|
-
| { type: "taskgroup"; step: number; tools: { uuid: string; updates: MessageToolUpdate[] }[] };
|
|
143
|
-
|
|
144
|
-
type ToolBlock = Extract<Block, { type: "tool" }>;
|
|
145
|
-
|
|
146
|
-
let blocks = $derived.by(() => {
|
|
147
|
-
const updates = message.updates ?? [];
|
|
148
|
-
const res: Block[] = [];
|
|
149
|
-
const hasTools = updates.some(isMessageToolUpdate);
|
|
150
|
-
let contentCursor = 0;
|
|
151
|
-
let sawFinalAnswer = false;
|
|
152
|
-
|
|
153
|
-
// Fast path: no tool updates at all
|
|
154
|
-
if (!hasTools && updates.length === 0) {
|
|
155
|
-
if (message.content) return [{ type: "text" as const, content: message.content }];
|
|
156
|
-
return [];
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
for (const update of updates) {
|
|
160
|
-
if (update.type === MessageUpdateType.Stream) {
|
|
161
|
-
const token =
|
|
162
|
-
typeof update.token === "string" && update.token.length > 0 ? update.token : null;
|
|
163
|
-
const len = token !== null ? token.length : (update.len ?? 0);
|
|
164
|
-
const chunk =
|
|
165
|
-
token ??
|
|
166
|
-
(message.content ? message.content.slice(contentCursor, contentCursor + len) : "");
|
|
167
|
-
contentCursor += len;
|
|
168
|
-
if (!chunk) continue;
|
|
169
|
-
const last = res.at(-1);
|
|
170
|
-
if (last?.type === "text") last.content += chunk;
|
|
171
|
-
else res.push({ type: "text" as const, content: chunk });
|
|
172
|
-
} else if (isMessageToolUpdate(update)) {
|
|
173
|
-
const existingBlock = res.find(
|
|
174
|
-
(b): b is ToolBlock => b.type === "tool" && b.uuid === update.uuid
|
|
175
|
-
);
|
|
176
|
-
if (existingBlock) {
|
|
177
|
-
existingBlock.updates.push(update);
|
|
178
|
-
} else {
|
|
179
|
-
res.push({ type: "tool" as const, uuid: update.uuid, updates: [update] });
|
|
180
|
-
}
|
|
181
|
-
} else if (update.type === MessageUpdateType.FinalAnswer) {
|
|
182
|
-
sawFinalAnswer = true;
|
|
183
|
-
const finalText = update.text ?? "";
|
|
184
|
-
const currentText = res
|
|
185
|
-
.filter((b) => b.type === "text")
|
|
186
|
-
.map((b) => (b as { type: "text"; content: string }).content)
|
|
187
|
-
.join("");
|
|
188
|
-
|
|
189
|
-
let addedText = "";
|
|
190
|
-
if (finalText.startsWith(currentText)) {
|
|
191
|
-
addedText = finalText.slice(currentText.length);
|
|
192
|
-
} else if (!currentText.endsWith(finalText)) {
|
|
193
|
-
const needsGap = !/\n\n$/.test(currentText) && !/^\n/.test(finalText);
|
|
194
|
-
addedText = (needsGap ? "\n\n" : "") + finalText;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
if (addedText) {
|
|
198
|
-
const last = res.at(-1);
|
|
199
|
-
if (last?.type === "text") {
|
|
200
|
-
last.content += addedText;
|
|
201
|
-
} else {
|
|
202
|
-
res.push({ type: "text" as const, content: addedText });
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// If content remains unmatched (e.g., persisted stream markers), append the remainder
|
|
209
|
-
// Skip when a FinalAnswer already provided the authoritative text.
|
|
210
|
-
if (!sawFinalAnswer && message.content && contentCursor < message.content.length) {
|
|
211
|
-
const remaining = message.content.slice(contentCursor);
|
|
212
|
-
if (remaining.length > 0) {
|
|
213
|
-
const last = res.at(-1);
|
|
214
|
-
if (last?.type === "text") last.content += remaining;
|
|
215
|
-
else res.push({ type: "text" as const, content: remaining });
|
|
216
|
-
}
|
|
217
|
-
} else if (!res.some((b) => b.type === "text") && message.content) {
|
|
218
|
-
// Fallback: no text produced at all
|
|
219
|
-
res.push({ type: "text" as const, content: message.content });
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
// Group consecutive tool blocks into TaskGroups for parallel display
|
|
223
|
-
const grouped: Block[] = [];
|
|
224
|
-
let pendingTools: { uuid: string; updates: MessageToolUpdate[] }[] = [];
|
|
225
|
-
let stepCounter = 0;
|
|
226
|
-
|
|
227
|
-
const flushTools = () => {
|
|
228
|
-
if (pendingTools.length === 0) return;
|
|
229
|
-
if (pendingTools.length === 1) {
|
|
230
|
-
// Single tool — render as regular ToolUpdate (no group wrapper)
|
|
231
|
-
grouped.push({ type: "tool", ...pendingTools[0] });
|
|
232
|
-
} else {
|
|
233
|
-
// Multiple consecutive tools — group them
|
|
234
|
-
stepCounter++;
|
|
235
|
-
grouped.push({ type: "taskgroup", step: stepCounter, tools: [...pendingTools] });
|
|
236
|
-
}
|
|
237
|
-
pendingTools = [];
|
|
238
|
-
};
|
|
239
|
-
|
|
240
|
-
for (const block of res) {
|
|
241
|
-
if (block.type === "tool") {
|
|
242
|
-
pendingTools.push({ uuid: block.uuid, updates: block.updates });
|
|
243
|
-
} else {
|
|
244
|
-
flushTools();
|
|
245
|
-
grouped.push(block);
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
flushTools();
|
|
249
|
-
|
|
250
|
-
return grouped;
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
$effect(() => {
|
|
254
|
-
if (isCopied) {
|
|
255
|
-
setTimeout(() => {
|
|
256
|
-
isCopied = false;
|
|
257
|
-
}, 1000);
|
|
258
|
-
}
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
let editMode = $derived(editMsdgId === message.id);
|
|
262
|
-
$effect(() => {
|
|
263
|
-
if (editMode) {
|
|
264
|
-
tick();
|
|
265
|
-
if (editContentEl) {
|
|
266
|
-
editContentEl.value = message.content;
|
|
267
|
-
editContentEl?.focus();
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
});
|
|
271
|
-
</script>
|
|
272
|
-
|
|
273
|
-
{#if message.from === "assistant"}
|
|
274
|
-
<div
|
|
275
|
-
bind:offsetWidth={messageWidth}
|
|
276
|
-
class="group relative -mb-4 flex w-fit max-w-full items-start justify-start gap-4 pb-4 leading-relaxed max-sm:mb-1 {message.routerMetadata &&
|
|
277
|
-
messageInfoWidth >= messageWidth
|
|
278
|
-
? 'mb-1'
|
|
279
|
-
: ''}"
|
|
280
|
-
data-message-id={message.id}
|
|
281
|
-
data-message-role="assistant"
|
|
282
|
-
role="presentation"
|
|
283
|
-
onclick={() => (isTapped = !isTapped)}
|
|
284
|
-
onkeydown={() => (isTapped = !isTapped)}
|
|
285
|
-
>
|
|
286
|
-
<MessageAvatar
|
|
287
|
-
classNames="mt-5 size-3.5 flex-none select-none rounded-full shadow-lg max-sm:hidden"
|
|
288
|
-
animating={isLast && loading}
|
|
289
|
-
/>
|
|
290
|
-
<div
|
|
291
|
-
class="relative flex min-w-[60px] flex-col gap-2 break-words rounded-2xl border border-gray-100 bg-gradient-to-br from-gray-50 px-5 py-3.5 text-gray-600 prose-pre:my-2 dark:border-gray-800 dark:from-gray-800/80 dark:text-gray-300"
|
|
292
|
-
>
|
|
293
|
-
{#if message.files?.length}
|
|
294
|
-
<div class="flex h-fit flex-wrap gap-x-5 gap-y-2">
|
|
295
|
-
{#each message.files as file (file.value)}
|
|
296
|
-
<UploadedFile {file} canClose={false} />
|
|
297
|
-
{/each}
|
|
298
|
-
</div>
|
|
299
|
-
{/if}
|
|
300
|
-
|
|
301
|
-
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
|
|
302
|
-
<div bind:this={contentEl} oncopy={handleCopy} onclick={handleContentClick}>
|
|
303
|
-
{#if isLast && loading && blocks.length === 0}
|
|
304
|
-
<IconLoading classNames="loading inline ml-2 first:ml-0" />
|
|
305
|
-
{/if}
|
|
306
|
-
{#each blocks as block, blockIndex (block.type === "tool" ? `${block.uuid}-${blockIndex}` : block.type === "taskgroup" ? `tg-${block.step}-${blockIndex}` : `text-${blockIndex}`)}
|
|
307
|
-
{@const nextBlock = blocks[blockIndex + 1]}
|
|
308
|
-
{@const nextBlockHasThink =
|
|
309
|
-
nextBlock?.type === "text" && THINK_BLOCK_TEST_REGEX.test(nextBlock.content)}
|
|
310
|
-
{@const nextIsLinkable = nextBlock?.type === "tool" || nextBlock?.type === "taskgroup" || nextBlockHasThink}
|
|
311
|
-
{#if block.type === "taskgroup"}
|
|
312
|
-
<div data-exclude-from-copy class="has-[+.prose]:mb-3 [.prose+&]:mt-4">
|
|
313
|
-
<TaskGroup step={block.step} tools={block.tools} {loading} />
|
|
314
|
-
</div>
|
|
315
|
-
{:else if block.type === "tool"}
|
|
316
|
-
<div data-exclude-from-copy class="has-[+.prose]:mb-3 [.prose+&]:mt-4">
|
|
317
|
-
<ToolUpdate tool={block.updates} {loading} hasNext={nextIsLinkable} />
|
|
318
|
-
</div>
|
|
319
|
-
{:else if block.type === "text"}
|
|
320
|
-
{#if isLast && loading && block.content.length === 0}
|
|
321
|
-
<IconLoading classNames="loading inline ml-2 first:ml-0" />
|
|
322
|
-
{/if}
|
|
323
|
-
|
|
324
|
-
{#if hasClientThink}
|
|
325
|
-
{@const parts = block.content.split(THINK_BLOCK_REGEX)}
|
|
326
|
-
{#each parts as part, partIndex}
|
|
327
|
-
{@const remainingParts = parts.slice(partIndex + 1)}
|
|
328
|
-
{@const hasMoreLinkable =
|
|
329
|
-
remainingParts.some((p) => p && THINK_BLOCK_TEST_REGEX.test(p)) || nextIsLinkable}
|
|
330
|
-
{#if part && part.startsWith("<think>")}
|
|
331
|
-
{@const isClosed = part.endsWith("</think>")}
|
|
332
|
-
{@const thinkContent = part.slice(7, isClosed ? -8 : undefined)}
|
|
333
|
-
|
|
334
|
-
<OpenReasoningResults
|
|
335
|
-
content={thinkContent}
|
|
336
|
-
loading={isLast && loading && !isClosed}
|
|
337
|
-
hasNext={hasMoreLinkable}
|
|
338
|
-
/>
|
|
339
|
-
{:else if part && part.trim().length > 0}
|
|
340
|
-
<div
|
|
341
|
-
class="prose max-w-none dark:prose-invert prose-headings:font-semibold prose-h1:text-lg prose-h2:text-base prose-h3:text-base prose-pre:bg-gray-800 prose-img:my-0 prose-img:cursor-pointer prose-img:rounded-lg dark:prose-pre:bg-gray-900"
|
|
342
|
-
>
|
|
343
|
-
<MarkdownRenderer content={part} loading={isLast && loading} />
|
|
344
|
-
</div>
|
|
345
|
-
{/if}
|
|
346
|
-
{/each}
|
|
347
|
-
{:else}
|
|
348
|
-
<div
|
|
349
|
-
class="prose max-w-none dark:prose-invert prose-headings:font-semibold prose-h1:text-lg prose-h2:text-base prose-h3:text-base prose-pre:bg-gray-800 prose-img:my-0 prose-img:cursor-pointer prose-img:rounded-lg dark:prose-pre:bg-gray-900"
|
|
350
|
-
>
|
|
351
|
-
<MarkdownRenderer content={block.content} loading={isLast && loading} />
|
|
352
|
-
</div>
|
|
353
|
-
{/if}
|
|
354
|
-
{/if}
|
|
355
|
-
{/each}
|
|
356
|
-
</div>
|
|
357
|
-
</div>
|
|
358
|
-
|
|
359
|
-
{#if message.routerMetadata || (!loading && message.content)}
|
|
360
|
-
<div
|
|
361
|
-
class="absolute -bottom-3.5 {message.routerMetadata && messageInfoWidth > messageWidth
|
|
362
|
-
? 'left-1 pl-1 lg:pl-7'
|
|
363
|
-
: 'right-1'} flex max-w-[calc(100dvw-40px)] items-center gap-0.5"
|
|
364
|
-
bind:offsetWidth={messageInfoWidth}
|
|
365
|
-
>
|
|
366
|
-
{#if message.routerMetadata && (message.routerMetadata.route || message.routerMetadata.model || message.routerMetadata.provider) && (!isLast || !loading)}
|
|
367
|
-
<div
|
|
368
|
-
class="mr-2 flex items-center gap-1.5 truncate whitespace-nowrap text-[.65rem] text-gray-400 dark:text-gray-400 sm:text-xs"
|
|
369
|
-
>
|
|
370
|
-
{#if message.routerMetadata.route && message.routerMetadata.model}
|
|
371
|
-
<span class="truncate rounded bg-gray-100 px-1 font-mono dark:bg-gray-800 sm:py-px">
|
|
372
|
-
{message.routerMetadata.route}
|
|
373
|
-
</span>
|
|
374
|
-
<span class="text-gray-500">with</span>
|
|
375
|
-
{#if publicConfig.isHuggingChat}
|
|
376
|
-
<a
|
|
377
|
-
href="/chat/settings/{message.routerMetadata.model}"
|
|
378
|
-
class="flex items-center gap-1 truncate rounded bg-gray-100 px-1 font-mono hover:text-gray-500 dark:bg-gray-800 dark:hover:text-gray-300 sm:py-px"
|
|
379
|
-
>
|
|
380
|
-
{message.routerMetadata.model.split("/").pop()}
|
|
381
|
-
</a>
|
|
382
|
-
{:else}
|
|
383
|
-
<span
|
|
384
|
-
class="truncate rounded bg-gray-100 px-1.5 font-mono dark:bg-gray-800 sm:py-px"
|
|
385
|
-
>
|
|
386
|
-
{message.routerMetadata.model.split("/").pop()}
|
|
387
|
-
</span>
|
|
388
|
-
{/if}
|
|
389
|
-
{/if}
|
|
390
|
-
{#if message.routerMetadata.provider}
|
|
391
|
-
{@const hubOrg = PROVIDERS_HUB_ORGS[message.routerMetadata.provider]}
|
|
392
|
-
<span class="text-gray-500 max-sm:hidden">via</span>
|
|
393
|
-
<a
|
|
394
|
-
target="_blank"
|
|
395
|
-
href="https://huggingface.co/{hubOrg}"
|
|
396
|
-
class="flex items-center gap-1 truncate rounded bg-gray-100 px-1 font-mono hover:text-gray-500 dark:bg-gray-800 dark:hover:text-gray-300 max-sm:hidden sm:py-px"
|
|
397
|
-
>
|
|
398
|
-
<img
|
|
399
|
-
src="https://huggingface.co/api/avatars/{hubOrg}"
|
|
400
|
-
alt="{message.routerMetadata.provider} logo"
|
|
401
|
-
class="size-2.5 flex-none rounded-sm"
|
|
402
|
-
onerror={(e) => ((e.currentTarget as HTMLImageElement).style.display = "none")}
|
|
403
|
-
/>
|
|
404
|
-
{message.routerMetadata.provider}
|
|
405
|
-
</a>
|
|
406
|
-
{/if}
|
|
407
|
-
</div>
|
|
408
|
-
{/if}
|
|
409
|
-
{#if !isLast || !loading}
|
|
410
|
-
<CopyToClipBoardBtn
|
|
411
|
-
onClick={() => {
|
|
412
|
-
isCopied = true;
|
|
413
|
-
}}
|
|
414
|
-
classNames="btn rounded-sm p-1 text-sm text-gray-400 hover:text-gray-500 focus:ring-0 dark:text-gray-400 dark:hover:text-gray-300"
|
|
415
|
-
value={contentWithoutThink}
|
|
416
|
-
iconClassNames="text-xs"
|
|
417
|
-
/>
|
|
418
|
-
<button
|
|
419
|
-
class="btn rounded-sm p-1 text-xs text-gray-400 hover:text-gray-500 focus:ring-0 dark:text-gray-400 dark:hover:text-gray-300"
|
|
420
|
-
title="Retry"
|
|
421
|
-
type="button"
|
|
422
|
-
onclick={() => {
|
|
423
|
-
onretry?.({ id: message.id });
|
|
424
|
-
}}
|
|
425
|
-
>
|
|
426
|
-
<CarbonRotate360 />
|
|
427
|
-
</button>
|
|
428
|
-
{#if alternatives.length > 1 && editMsdgId === null}
|
|
429
|
-
<Alternatives
|
|
430
|
-
{message}
|
|
431
|
-
{alternatives}
|
|
432
|
-
{loading}
|
|
433
|
-
onshowAlternateMsg={(payload) => onshowAlternateMsg?.(payload)}
|
|
434
|
-
/>
|
|
435
|
-
{/if}
|
|
436
|
-
{/if}
|
|
437
|
-
</div>
|
|
438
|
-
{/if}
|
|
439
|
-
</div>
|
|
440
|
-
{#if lightboxSrc}
|
|
441
|
-
<ImageLightbox src={lightboxSrc} onclose={() => (lightboxSrc = null)} />
|
|
442
|
-
{/if}
|
|
443
|
-
{/if}
|
|
444
|
-
{#if message.from === "user"}
|
|
445
|
-
<div
|
|
446
|
-
class="group relative {alternatives.length > 1 && editMsdgId === null
|
|
447
|
-
? 'mb-7'
|
|
448
|
-
: ''} w-full items-start justify-start gap-4"
|
|
449
|
-
data-message-id={message.id}
|
|
450
|
-
data-message-type="user"
|
|
451
|
-
role="presentation"
|
|
452
|
-
onclick={() => (isTapped = !isTapped)}
|
|
453
|
-
onkeydown={() => (isTapped = !isTapped)}
|
|
454
|
-
>
|
|
455
|
-
<div class="flex w-full flex-col gap-2">
|
|
456
|
-
{#if message.files?.length}
|
|
457
|
-
<div class="flex w-fit gap-4 px-5">
|
|
458
|
-
{#each message.files as file}
|
|
459
|
-
<UploadedFile {file} canClose={false} />
|
|
460
|
-
{/each}
|
|
461
|
-
</div>
|
|
462
|
-
{/if}
|
|
463
|
-
|
|
464
|
-
<div class="flex w-full flex-row flex-nowrap">
|
|
465
|
-
{#if !editMode}
|
|
466
|
-
<p
|
|
467
|
-
class="disabled w-full appearance-none whitespace-break-spaces text-wrap break-words bg-inherit px-5 py-3.5 text-gray-500 dark:text-gray-400"
|
|
468
|
-
>
|
|
469
|
-
{message.content.trim()}
|
|
470
|
-
</p>
|
|
471
|
-
{:else}
|
|
472
|
-
<form
|
|
473
|
-
class="mt-3 flex w-full flex-col"
|
|
474
|
-
bind:this={editFormEl}
|
|
475
|
-
onsubmit={(e) => {
|
|
476
|
-
e.preventDefault();
|
|
477
|
-
onretry?.({ content: editContentEl?.value, id: message.id });
|
|
478
|
-
editMsdgId = null;
|
|
479
|
-
}}
|
|
480
|
-
>
|
|
481
|
-
<textarea
|
|
482
|
-
class="w-full whitespace-break-spaces break-words rounded-xl bg-gray-100 px-5 py-3.5 text-gray-500 *:h-max focus:outline-none dark:bg-gray-800 dark:text-gray-400"
|
|
483
|
-
rows="5"
|
|
484
|
-
bind:this={editContentEl}
|
|
485
|
-
value={message.content.trim()}
|
|
486
|
-
onkeydown={handleKeyDown}
|
|
487
|
-
required
|
|
488
|
-
></textarea>
|
|
489
|
-
<div class="flex w-full flex-row flex-nowrap items-center justify-center gap-2 pt-2">
|
|
490
|
-
<button
|
|
491
|
-
type="submit"
|
|
492
|
-
class="btn rounded-lg px-3 py-1.5 text-sm
|
|
493
|
-
{loading
|
|
494
|
-
? 'bg-gray-300 text-gray-400 dark:bg-gray-700 dark:text-gray-600'
|
|
495
|
-
: 'bg-gray-200 text-gray-600 hover:text-gray-800 focus:ring-0 dark:bg-gray-800 dark:text-gray-300 dark:hover:text-gray-200'}
|
|
496
|
-
"
|
|
497
|
-
disabled={loading}
|
|
498
|
-
>
|
|
499
|
-
Send
|
|
500
|
-
</button>
|
|
501
|
-
<button
|
|
502
|
-
type="button"
|
|
503
|
-
class="btn rounded-sm p-2 text-sm text-gray-400 hover:text-gray-500 focus:ring-0 dark:text-gray-400 dark:hover:text-gray-300"
|
|
504
|
-
onclick={() => {
|
|
505
|
-
editMsdgId = null;
|
|
506
|
-
}}
|
|
507
|
-
>
|
|
508
|
-
Cancel
|
|
509
|
-
</button>
|
|
510
|
-
</div>
|
|
511
|
-
</form>
|
|
512
|
-
{/if}
|
|
513
|
-
</div>
|
|
514
|
-
<div class="absolute -bottom-4 ml-3.5 flex w-full gap-1.5">
|
|
515
|
-
{#if alternatives.length > 1 && editMsdgId === null}
|
|
516
|
-
<Alternatives
|
|
517
|
-
{message}
|
|
518
|
-
{alternatives}
|
|
519
|
-
{loading}
|
|
520
|
-
onshowAlternateMsg={(payload) => onshowAlternateMsg?.(payload)}
|
|
521
|
-
/>
|
|
522
|
-
{/if}
|
|
523
|
-
{#if (alternatives.length > 1 && editMsdgId === null) || (!loading && !editMode)}
|
|
524
|
-
<button
|
|
525
|
-
class="hidden cursor-pointer items-center gap-1 rounded-md border border-gray-200 px-1.5 py-0.5 text-xs text-gray-400 group-hover:flex hover:flex hover:text-gray-500 dark:border-gray-700 dark:text-gray-400 dark:hover:text-gray-300 lg:-right-2"
|
|
526
|
-
title="Edit"
|
|
527
|
-
type="button"
|
|
528
|
-
onclick={() => {
|
|
529
|
-
if (requireAuthUser()) return;
|
|
530
|
-
editMsdgId = message.id;
|
|
531
|
-
}}
|
|
532
|
-
>
|
|
533
|
-
<CarbonPen />
|
|
534
|
-
Edit
|
|
535
|
-
</button>
|
|
536
|
-
{/if}
|
|
537
|
-
</div>
|
|
538
|
-
</div>
|
|
539
|
-
</div>
|
|
540
|
-
{/if}
|
|
541
|
-
|
|
542
|
-
<style>
|
|
543
|
-
@keyframes loading {
|
|
544
|
-
to {
|
|
545
|
-
stroke-dashoffset: 122.9;
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
</style>
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Message } from "$lib/types/Message";
|
|
3
|
+
import { tick } from "svelte";
|
|
4
|
+
|
|
5
|
+
import { usePublicConfig } from "$lib/utils/PublicConfig.svelte";
|
|
6
|
+
const publicConfig = usePublicConfig();
|
|
7
|
+
import CopyToClipBoardBtn from "../CopyToClipBoardBtn.svelte";
|
|
8
|
+
import IconLoading from "../icons/IconLoading.svelte";
|
|
9
|
+
import CarbonRotate360 from "~icons/carbon/rotate-360";
|
|
10
|
+
// import CarbonDownload from "~icons/carbon/download";
|
|
11
|
+
|
|
12
|
+
import CarbonPen from "~icons/carbon/pen";
|
|
13
|
+
import UploadedFile from "./UploadedFile.svelte";
|
|
14
|
+
|
|
15
|
+
import MarkdownRenderer from "./MarkdownRenderer.svelte";
|
|
16
|
+
import OpenReasoningResults from "./OpenReasoningResults.svelte";
|
|
17
|
+
import Alternatives from "./Alternatives.svelte";
|
|
18
|
+
import MessageAvatar from "./MessageAvatar.svelte";
|
|
19
|
+
import { PROVIDERS_HUB_ORGS } from "@huggingface/inference";
|
|
20
|
+
import { requireAuthUser } from "$lib/utils/auth";
|
|
21
|
+
import ToolUpdate from "./ToolUpdate.svelte";
|
|
22
|
+
import TaskGroup from "./TaskGroup.svelte";
|
|
23
|
+
import { isMessageToolUpdate } from "$lib/utils/messageUpdates";
|
|
24
|
+
import { MessageUpdateType, type MessageToolUpdate } from "$lib/types/MessageUpdate";
|
|
25
|
+
import ImageLightbox from "./ImageLightbox.svelte";
|
|
26
|
+
|
|
27
|
+
interface Props {
|
|
28
|
+
message: Message;
|
|
29
|
+
loading?: boolean;
|
|
30
|
+
isAuthor?: boolean;
|
|
31
|
+
readOnly?: boolean;
|
|
32
|
+
isTapped?: boolean;
|
|
33
|
+
alternatives?: Message["id"][];
|
|
34
|
+
editMsdgId?: Message["id"] | null;
|
|
35
|
+
isLast?: boolean;
|
|
36
|
+
onretry?: (payload: { id: Message["id"]; content?: string }) => void;
|
|
37
|
+
onshowAlternateMsg?: (payload: { id: Message["id"] }) => void;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let {
|
|
41
|
+
message,
|
|
42
|
+
loading = false,
|
|
43
|
+
isAuthor: _isAuthor = true,
|
|
44
|
+
readOnly: _readOnly = false,
|
|
45
|
+
isTapped = $bindable(false),
|
|
46
|
+
alternatives = [],
|
|
47
|
+
editMsdgId = $bindable(null),
|
|
48
|
+
isLast = false,
|
|
49
|
+
onretry,
|
|
50
|
+
onshowAlternateMsg,
|
|
51
|
+
}: Props = $props();
|
|
52
|
+
|
|
53
|
+
let contentEl: HTMLElement | undefined = $state();
|
|
54
|
+
let isCopied = $state(false);
|
|
55
|
+
let messageWidth: number = $state(0);
|
|
56
|
+
let messageInfoWidth: number = $state(0);
|
|
57
|
+
let lightboxSrc: string | null = $state(null);
|
|
58
|
+
|
|
59
|
+
function handleContentClick(e: MouseEvent) {
|
|
60
|
+
const target = e.target as HTMLElement;
|
|
61
|
+
if (target.tagName === "IMG" && target instanceof HTMLImageElement) {
|
|
62
|
+
e.preventDefault();
|
|
63
|
+
e.stopPropagation();
|
|
64
|
+
lightboxSrc = target.src;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
$effect(() => {
|
|
69
|
+
// referenced to appease linter for currently-unused props
|
|
70
|
+
void _isAuthor;
|
|
71
|
+
void _readOnly;
|
|
72
|
+
});
|
|
73
|
+
function handleKeyDown(e: KeyboardEvent) {
|
|
74
|
+
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
|
|
75
|
+
editFormEl?.requestSubmit();
|
|
76
|
+
}
|
|
77
|
+
if (e.key === "Escape") {
|
|
78
|
+
editMsdgId = null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function handleCopy(event: ClipboardEvent) {
|
|
83
|
+
if (!contentEl) return;
|
|
84
|
+
|
|
85
|
+
const selection = window.getSelection();
|
|
86
|
+
if (!selection || selection.isCollapsed) return;
|
|
87
|
+
if (!selection.anchorNode || !selection.focusNode) return;
|
|
88
|
+
|
|
89
|
+
const anchorInside = contentEl.contains(selection.anchorNode);
|
|
90
|
+
const focusInside = contentEl.contains(selection.focusNode);
|
|
91
|
+
if (!anchorInside && !focusInside) return;
|
|
92
|
+
|
|
93
|
+
if (!event.clipboardData) return;
|
|
94
|
+
|
|
95
|
+
const range = selection.getRangeAt(0);
|
|
96
|
+
const wrapper = document.createElement("div");
|
|
97
|
+
wrapper.appendChild(range.cloneContents());
|
|
98
|
+
|
|
99
|
+
wrapper.querySelectorAll("[data-exclude-from-copy]").forEach((el) => {
|
|
100
|
+
el.remove();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
wrapper.querySelectorAll("*").forEach((el) => {
|
|
104
|
+
el.removeAttribute("style");
|
|
105
|
+
el.removeAttribute("class");
|
|
106
|
+
el.removeAttribute("color");
|
|
107
|
+
el.removeAttribute("bgcolor");
|
|
108
|
+
el.removeAttribute("background");
|
|
109
|
+
|
|
110
|
+
for (const attr of Array.from(el.attributes)) {
|
|
111
|
+
if (attr.name === "id" || attr.name.startsWith("data-")) {
|
|
112
|
+
el.removeAttribute(attr.name);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const html = wrapper.innerHTML;
|
|
118
|
+
const text = wrapper.textContent ?? "";
|
|
119
|
+
|
|
120
|
+
event.preventDefault();
|
|
121
|
+
event.clipboardData.setData("text/html", html);
|
|
122
|
+
event.clipboardData.setData("text/plain", text);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
let editContentEl: HTMLTextAreaElement | undefined = $state();
|
|
126
|
+
let editFormEl: HTMLFormElement | undefined = $state();
|
|
127
|
+
|
|
128
|
+
// Zero-config reasoning autodetection: detect <think> blocks in content
|
|
129
|
+
const THINK_BLOCK_REGEX = /(<think>[\s\S]*?(?:<\/think>|$))/gi;
|
|
130
|
+
// Non-global version for .test() calls to avoid lastIndex side effects
|
|
131
|
+
const THINK_BLOCK_TEST_REGEX = /(<think>[\s\S]*?(?:<\/think>|$))/i;
|
|
132
|
+
let hasClientThink = $derived(message.content.split(THINK_BLOCK_REGEX).length > 1);
|
|
133
|
+
|
|
134
|
+
// Strip think blocks for clipboard copy (always, regardless of detection)
|
|
135
|
+
let contentWithoutThink = $derived.by(() =>
|
|
136
|
+
message.content.replace(THINK_BLOCK_REGEX, "").trim()
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
type Block =
|
|
140
|
+
| { type: "text"; content: string }
|
|
141
|
+
| { type: "tool"; uuid: string; updates: MessageToolUpdate[] }
|
|
142
|
+
| { type: "taskgroup"; step: number; tools: { uuid: string; updates: MessageToolUpdate[] }[] };
|
|
143
|
+
|
|
144
|
+
type ToolBlock = Extract<Block, { type: "tool" }>;
|
|
145
|
+
|
|
146
|
+
let blocks = $derived.by(() => {
|
|
147
|
+
const updates = message.updates ?? [];
|
|
148
|
+
const res: Block[] = [];
|
|
149
|
+
const hasTools = updates.some(isMessageToolUpdate);
|
|
150
|
+
let contentCursor = 0;
|
|
151
|
+
let sawFinalAnswer = false;
|
|
152
|
+
|
|
153
|
+
// Fast path: no tool updates at all
|
|
154
|
+
if (!hasTools && updates.length === 0) {
|
|
155
|
+
if (message.content) return [{ type: "text" as const, content: message.content }];
|
|
156
|
+
return [];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
for (const update of updates) {
|
|
160
|
+
if (update.type === MessageUpdateType.Stream) {
|
|
161
|
+
const token =
|
|
162
|
+
typeof update.token === "string" && update.token.length > 0 ? update.token : null;
|
|
163
|
+
const len = token !== null ? token.length : (update.len ?? 0);
|
|
164
|
+
const chunk =
|
|
165
|
+
token ??
|
|
166
|
+
(message.content ? message.content.slice(contentCursor, contentCursor + len) : "");
|
|
167
|
+
contentCursor += len;
|
|
168
|
+
if (!chunk) continue;
|
|
169
|
+
const last = res.at(-1);
|
|
170
|
+
if (last?.type === "text") last.content += chunk;
|
|
171
|
+
else res.push({ type: "text" as const, content: chunk });
|
|
172
|
+
} else if (isMessageToolUpdate(update)) {
|
|
173
|
+
const existingBlock = res.find(
|
|
174
|
+
(b): b is ToolBlock => b.type === "tool" && b.uuid === update.uuid
|
|
175
|
+
);
|
|
176
|
+
if (existingBlock) {
|
|
177
|
+
existingBlock.updates.push(update);
|
|
178
|
+
} else {
|
|
179
|
+
res.push({ type: "tool" as const, uuid: update.uuid, updates: [update] });
|
|
180
|
+
}
|
|
181
|
+
} else if (update.type === MessageUpdateType.FinalAnswer) {
|
|
182
|
+
sawFinalAnswer = true;
|
|
183
|
+
const finalText = update.text ?? "";
|
|
184
|
+
const currentText = res
|
|
185
|
+
.filter((b) => b.type === "text")
|
|
186
|
+
.map((b) => (b as { type: "text"; content: string }).content)
|
|
187
|
+
.join("");
|
|
188
|
+
|
|
189
|
+
let addedText = "";
|
|
190
|
+
if (finalText.startsWith(currentText)) {
|
|
191
|
+
addedText = finalText.slice(currentText.length);
|
|
192
|
+
} else if (!currentText.endsWith(finalText)) {
|
|
193
|
+
const needsGap = !/\n\n$/.test(currentText) && !/^\n/.test(finalText);
|
|
194
|
+
addedText = (needsGap ? "\n\n" : "") + finalText;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (addedText) {
|
|
198
|
+
const last = res.at(-1);
|
|
199
|
+
if (last?.type === "text") {
|
|
200
|
+
last.content += addedText;
|
|
201
|
+
} else {
|
|
202
|
+
res.push({ type: "text" as const, content: addedText });
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// If content remains unmatched (e.g., persisted stream markers), append the remainder
|
|
209
|
+
// Skip when a FinalAnswer already provided the authoritative text.
|
|
210
|
+
if (!sawFinalAnswer && message.content && contentCursor < message.content.length) {
|
|
211
|
+
const remaining = message.content.slice(contentCursor);
|
|
212
|
+
if (remaining.length > 0) {
|
|
213
|
+
const last = res.at(-1);
|
|
214
|
+
if (last?.type === "text") last.content += remaining;
|
|
215
|
+
else res.push({ type: "text" as const, content: remaining });
|
|
216
|
+
}
|
|
217
|
+
} else if (!res.some((b) => b.type === "text") && message.content) {
|
|
218
|
+
// Fallback: no text produced at all
|
|
219
|
+
res.push({ type: "text" as const, content: message.content });
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Group consecutive tool blocks into TaskGroups for parallel display
|
|
223
|
+
const grouped: Block[] = [];
|
|
224
|
+
let pendingTools: { uuid: string; updates: MessageToolUpdate[] }[] = [];
|
|
225
|
+
let stepCounter = 0;
|
|
226
|
+
|
|
227
|
+
const flushTools = () => {
|
|
228
|
+
if (pendingTools.length === 0) return;
|
|
229
|
+
if (pendingTools.length === 1) {
|
|
230
|
+
// Single tool — render as regular ToolUpdate (no group wrapper)
|
|
231
|
+
grouped.push({ type: "tool", ...pendingTools[0] });
|
|
232
|
+
} else {
|
|
233
|
+
// Multiple consecutive tools — group them
|
|
234
|
+
stepCounter++;
|
|
235
|
+
grouped.push({ type: "taskgroup", step: stepCounter, tools: [...pendingTools] });
|
|
236
|
+
}
|
|
237
|
+
pendingTools = [];
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
for (const block of res) {
|
|
241
|
+
if (block.type === "tool") {
|
|
242
|
+
pendingTools.push({ uuid: block.uuid, updates: block.updates });
|
|
243
|
+
} else {
|
|
244
|
+
flushTools();
|
|
245
|
+
grouped.push(block);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
flushTools();
|
|
249
|
+
|
|
250
|
+
return grouped;
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
$effect(() => {
|
|
254
|
+
if (isCopied) {
|
|
255
|
+
setTimeout(() => {
|
|
256
|
+
isCopied = false;
|
|
257
|
+
}, 1000);
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
let editMode = $derived(editMsdgId === message.id);
|
|
262
|
+
$effect(() => {
|
|
263
|
+
if (editMode) {
|
|
264
|
+
tick();
|
|
265
|
+
if (editContentEl) {
|
|
266
|
+
editContentEl.value = message.content;
|
|
267
|
+
editContentEl?.focus();
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
</script>
|
|
272
|
+
|
|
273
|
+
{#if message.from === "assistant"}
|
|
274
|
+
<div
|
|
275
|
+
bind:offsetWidth={messageWidth}
|
|
276
|
+
class="group relative -mb-4 flex w-fit max-w-full items-start justify-start gap-4 pb-4 leading-relaxed max-sm:mb-1 {message.routerMetadata &&
|
|
277
|
+
messageInfoWidth >= messageWidth
|
|
278
|
+
? 'mb-1'
|
|
279
|
+
: ''}"
|
|
280
|
+
data-message-id={message.id}
|
|
281
|
+
data-message-role="assistant"
|
|
282
|
+
role="presentation"
|
|
283
|
+
onclick={() => (isTapped = !isTapped)}
|
|
284
|
+
onkeydown={() => (isTapped = !isTapped)}
|
|
285
|
+
>
|
|
286
|
+
<MessageAvatar
|
|
287
|
+
classNames="mt-5 size-3.5 flex-none select-none rounded-full shadow-lg max-sm:hidden"
|
|
288
|
+
animating={isLast && loading}
|
|
289
|
+
/>
|
|
290
|
+
<div
|
|
291
|
+
class="relative flex min-w-[60px] flex-col gap-2 break-words rounded-2xl border border-gray-100 bg-gradient-to-br from-gray-50 px-5 py-3.5 text-gray-600 prose-pre:my-2 dark:border-gray-800 dark:from-gray-800/80 dark:text-gray-300"
|
|
292
|
+
>
|
|
293
|
+
{#if message.files?.length}
|
|
294
|
+
<div class="flex h-fit flex-wrap gap-x-5 gap-y-2">
|
|
295
|
+
{#each message.files as file (file.value)}
|
|
296
|
+
<UploadedFile {file} canClose={false} />
|
|
297
|
+
{/each}
|
|
298
|
+
</div>
|
|
299
|
+
{/if}
|
|
300
|
+
|
|
301
|
+
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
|
|
302
|
+
<div bind:this={contentEl} oncopy={handleCopy} onclick={handleContentClick}>
|
|
303
|
+
{#if isLast && loading && blocks.length === 0}
|
|
304
|
+
<IconLoading classNames="loading inline ml-2 first:ml-0" />
|
|
305
|
+
{/if}
|
|
306
|
+
{#each blocks as block, blockIndex (block.type === "tool" ? `${block.uuid}-${blockIndex}` : block.type === "taskgroup" ? `tg-${block.step}-${blockIndex}` : `text-${blockIndex}`)}
|
|
307
|
+
{@const nextBlock = blocks[blockIndex + 1]}
|
|
308
|
+
{@const nextBlockHasThink =
|
|
309
|
+
nextBlock?.type === "text" && THINK_BLOCK_TEST_REGEX.test(nextBlock.content)}
|
|
310
|
+
{@const nextIsLinkable = nextBlock?.type === "tool" || nextBlock?.type === "taskgroup" || nextBlockHasThink}
|
|
311
|
+
{#if block.type === "taskgroup"}
|
|
312
|
+
<div data-exclude-from-copy class="has-[+.prose]:mb-3 [.prose+&]:mt-4">
|
|
313
|
+
<TaskGroup step={block.step} tools={block.tools} {loading} />
|
|
314
|
+
</div>
|
|
315
|
+
{:else if block.type === "tool"}
|
|
316
|
+
<div data-exclude-from-copy class="has-[+.prose]:mb-3 [.prose+&]:mt-4">
|
|
317
|
+
<ToolUpdate tool={block.updates} {loading} hasNext={nextIsLinkable} />
|
|
318
|
+
</div>
|
|
319
|
+
{:else if block.type === "text"}
|
|
320
|
+
{#if isLast && loading && block.content.length === 0}
|
|
321
|
+
<IconLoading classNames="loading inline ml-2 first:ml-0" />
|
|
322
|
+
{/if}
|
|
323
|
+
|
|
324
|
+
{#if hasClientThink}
|
|
325
|
+
{@const parts = block.content.split(THINK_BLOCK_REGEX)}
|
|
326
|
+
{#each parts as part, partIndex}
|
|
327
|
+
{@const remainingParts = parts.slice(partIndex + 1)}
|
|
328
|
+
{@const hasMoreLinkable =
|
|
329
|
+
remainingParts.some((p) => p && THINK_BLOCK_TEST_REGEX.test(p)) || nextIsLinkable}
|
|
330
|
+
{#if part && part.startsWith("<think>")}
|
|
331
|
+
{@const isClosed = part.endsWith("</think>")}
|
|
332
|
+
{@const thinkContent = part.slice(7, isClosed ? -8 : undefined)}
|
|
333
|
+
|
|
334
|
+
<OpenReasoningResults
|
|
335
|
+
content={thinkContent}
|
|
336
|
+
loading={isLast && loading && !isClosed}
|
|
337
|
+
hasNext={hasMoreLinkable}
|
|
338
|
+
/>
|
|
339
|
+
{:else if part && part.trim().length > 0}
|
|
340
|
+
<div
|
|
341
|
+
class="prose max-w-none dark:prose-invert prose-headings:font-semibold prose-h1:text-lg prose-h2:text-base prose-h3:text-base prose-pre:bg-gray-800 prose-img:my-0 prose-img:cursor-pointer prose-img:rounded-lg dark:prose-pre:bg-gray-900"
|
|
342
|
+
>
|
|
343
|
+
<MarkdownRenderer content={part} loading={isLast && loading} />
|
|
344
|
+
</div>
|
|
345
|
+
{/if}
|
|
346
|
+
{/each}
|
|
347
|
+
{:else}
|
|
348
|
+
<div
|
|
349
|
+
class="prose max-w-none dark:prose-invert prose-headings:font-semibold prose-h1:text-lg prose-h2:text-base prose-h3:text-base prose-pre:bg-gray-800 prose-img:my-0 prose-img:cursor-pointer prose-img:rounded-lg dark:prose-pre:bg-gray-900"
|
|
350
|
+
>
|
|
351
|
+
<MarkdownRenderer content={block.content} loading={isLast && loading} />
|
|
352
|
+
</div>
|
|
353
|
+
{/if}
|
|
354
|
+
{/if}
|
|
355
|
+
{/each}
|
|
356
|
+
</div>
|
|
357
|
+
</div>
|
|
358
|
+
|
|
359
|
+
{#if message.routerMetadata || (!loading && message.content)}
|
|
360
|
+
<div
|
|
361
|
+
class="absolute -bottom-3.5 {message.routerMetadata && messageInfoWidth > messageWidth
|
|
362
|
+
? 'left-1 pl-1 lg:pl-7'
|
|
363
|
+
: 'right-1'} flex max-w-[calc(100dvw-40px)] items-center gap-0.5"
|
|
364
|
+
bind:offsetWidth={messageInfoWidth}
|
|
365
|
+
>
|
|
366
|
+
{#if message.routerMetadata && (message.routerMetadata.route || message.routerMetadata.model || message.routerMetadata.provider) && (!isLast || !loading)}
|
|
367
|
+
<div
|
|
368
|
+
class="mr-2 flex items-center gap-1.5 truncate whitespace-nowrap text-[.65rem] text-gray-400 dark:text-gray-400 sm:text-xs"
|
|
369
|
+
>
|
|
370
|
+
{#if message.routerMetadata.route && message.routerMetadata.model}
|
|
371
|
+
<span class="truncate rounded bg-gray-100 px-1 font-mono dark:bg-gray-800 sm:py-px">
|
|
372
|
+
{message.routerMetadata.route}
|
|
373
|
+
</span>
|
|
374
|
+
<span class="text-gray-500">with</span>
|
|
375
|
+
{#if publicConfig.isHuggingChat}
|
|
376
|
+
<a
|
|
377
|
+
href="/chat/settings/{message.routerMetadata.model}"
|
|
378
|
+
class="flex items-center gap-1 truncate rounded bg-gray-100 px-1 font-mono hover:text-gray-500 dark:bg-gray-800 dark:hover:text-gray-300 sm:py-px"
|
|
379
|
+
>
|
|
380
|
+
{message.routerMetadata.model.split("/").pop()}
|
|
381
|
+
</a>
|
|
382
|
+
{:else}
|
|
383
|
+
<span
|
|
384
|
+
class="truncate rounded bg-gray-100 px-1.5 font-mono dark:bg-gray-800 sm:py-px"
|
|
385
|
+
>
|
|
386
|
+
{message.routerMetadata.model.split("/").pop()}
|
|
387
|
+
</span>
|
|
388
|
+
{/if}
|
|
389
|
+
{/if}
|
|
390
|
+
{#if message.routerMetadata.provider}
|
|
391
|
+
{@const hubOrg = PROVIDERS_HUB_ORGS[message.routerMetadata.provider]}
|
|
392
|
+
<span class="text-gray-500 max-sm:hidden">via</span>
|
|
393
|
+
<a
|
|
394
|
+
target="_blank"
|
|
395
|
+
href="https://huggingface.co/{hubOrg}"
|
|
396
|
+
class="flex items-center gap-1 truncate rounded bg-gray-100 px-1 font-mono hover:text-gray-500 dark:bg-gray-800 dark:hover:text-gray-300 max-sm:hidden sm:py-px"
|
|
397
|
+
>
|
|
398
|
+
<img
|
|
399
|
+
src="https://huggingface.co/api/avatars/{hubOrg}"
|
|
400
|
+
alt="{message.routerMetadata.provider} logo"
|
|
401
|
+
class="size-2.5 flex-none rounded-sm"
|
|
402
|
+
onerror={(e) => ((e.currentTarget as HTMLImageElement).style.display = "none")}
|
|
403
|
+
/>
|
|
404
|
+
{message.routerMetadata.provider}
|
|
405
|
+
</a>
|
|
406
|
+
{/if}
|
|
407
|
+
</div>
|
|
408
|
+
{/if}
|
|
409
|
+
{#if !isLast || !loading}
|
|
410
|
+
<CopyToClipBoardBtn
|
|
411
|
+
onClick={() => {
|
|
412
|
+
isCopied = true;
|
|
413
|
+
}}
|
|
414
|
+
classNames="btn rounded-sm p-1 text-sm text-gray-400 hover:text-gray-500 focus:ring-0 dark:text-gray-400 dark:hover:text-gray-300"
|
|
415
|
+
value={contentWithoutThink}
|
|
416
|
+
iconClassNames="text-xs"
|
|
417
|
+
/>
|
|
418
|
+
<button
|
|
419
|
+
class="btn rounded-sm p-1 text-xs text-gray-400 hover:text-gray-500 focus:ring-0 dark:text-gray-400 dark:hover:text-gray-300"
|
|
420
|
+
title="Retry"
|
|
421
|
+
type="button"
|
|
422
|
+
onclick={() => {
|
|
423
|
+
onretry?.({ id: message.id });
|
|
424
|
+
}}
|
|
425
|
+
>
|
|
426
|
+
<CarbonRotate360 />
|
|
427
|
+
</button>
|
|
428
|
+
{#if alternatives.length > 1 && editMsdgId === null}
|
|
429
|
+
<Alternatives
|
|
430
|
+
{message}
|
|
431
|
+
{alternatives}
|
|
432
|
+
{loading}
|
|
433
|
+
onshowAlternateMsg={(payload) => onshowAlternateMsg?.(payload)}
|
|
434
|
+
/>
|
|
435
|
+
{/if}
|
|
436
|
+
{/if}
|
|
437
|
+
</div>
|
|
438
|
+
{/if}
|
|
439
|
+
</div>
|
|
440
|
+
{#if lightboxSrc}
|
|
441
|
+
<ImageLightbox src={lightboxSrc} onclose={() => (lightboxSrc = null)} />
|
|
442
|
+
{/if}
|
|
443
|
+
{/if}
|
|
444
|
+
{#if message.from === "user"}
|
|
445
|
+
<div
|
|
446
|
+
class="group relative {alternatives.length > 1 && editMsdgId === null
|
|
447
|
+
? 'mb-7'
|
|
448
|
+
: ''} w-full items-start justify-start gap-4"
|
|
449
|
+
data-message-id={message.id}
|
|
450
|
+
data-message-type="user"
|
|
451
|
+
role="presentation"
|
|
452
|
+
onclick={() => (isTapped = !isTapped)}
|
|
453
|
+
onkeydown={() => (isTapped = !isTapped)}
|
|
454
|
+
>
|
|
455
|
+
<div class="flex w-full flex-col gap-2">
|
|
456
|
+
{#if message.files?.length}
|
|
457
|
+
<div class="flex w-fit gap-4 px-5">
|
|
458
|
+
{#each message.files as file}
|
|
459
|
+
<UploadedFile {file} canClose={false} />
|
|
460
|
+
{/each}
|
|
461
|
+
</div>
|
|
462
|
+
{/if}
|
|
463
|
+
|
|
464
|
+
<div class="flex w-full flex-row flex-nowrap">
|
|
465
|
+
{#if !editMode}
|
|
466
|
+
<p
|
|
467
|
+
class="disabled w-full appearance-none whitespace-break-spaces text-wrap break-words bg-inherit px-5 py-3.5 text-gray-500 dark:text-gray-400"
|
|
468
|
+
>
|
|
469
|
+
{message.content.trim()}
|
|
470
|
+
</p>
|
|
471
|
+
{:else}
|
|
472
|
+
<form
|
|
473
|
+
class="mt-3 flex w-full flex-col"
|
|
474
|
+
bind:this={editFormEl}
|
|
475
|
+
onsubmit={(e) => {
|
|
476
|
+
e.preventDefault();
|
|
477
|
+
onretry?.({ content: editContentEl?.value, id: message.id });
|
|
478
|
+
editMsdgId = null;
|
|
479
|
+
}}
|
|
480
|
+
>
|
|
481
|
+
<textarea
|
|
482
|
+
class="w-full whitespace-break-spaces break-words rounded-xl bg-gray-100 px-5 py-3.5 text-gray-500 *:h-max focus:outline-none dark:bg-gray-800 dark:text-gray-400"
|
|
483
|
+
rows="5"
|
|
484
|
+
bind:this={editContentEl}
|
|
485
|
+
value={message.content.trim()}
|
|
486
|
+
onkeydown={handleKeyDown}
|
|
487
|
+
required
|
|
488
|
+
></textarea>
|
|
489
|
+
<div class="flex w-full flex-row flex-nowrap items-center justify-center gap-2 pt-2">
|
|
490
|
+
<button
|
|
491
|
+
type="submit"
|
|
492
|
+
class="btn rounded-lg px-3 py-1.5 text-sm
|
|
493
|
+
{loading
|
|
494
|
+
? 'bg-gray-300 text-gray-400 dark:bg-gray-700 dark:text-gray-600'
|
|
495
|
+
: 'bg-gray-200 text-gray-600 hover:text-gray-800 focus:ring-0 dark:bg-gray-800 dark:text-gray-300 dark:hover:text-gray-200'}
|
|
496
|
+
"
|
|
497
|
+
disabled={loading}
|
|
498
|
+
>
|
|
499
|
+
Send
|
|
500
|
+
</button>
|
|
501
|
+
<button
|
|
502
|
+
type="button"
|
|
503
|
+
class="btn rounded-sm p-2 text-sm text-gray-400 hover:text-gray-500 focus:ring-0 dark:text-gray-400 dark:hover:text-gray-300"
|
|
504
|
+
onclick={() => {
|
|
505
|
+
editMsdgId = null;
|
|
506
|
+
}}
|
|
507
|
+
>
|
|
508
|
+
Cancel
|
|
509
|
+
</button>
|
|
510
|
+
</div>
|
|
511
|
+
</form>
|
|
512
|
+
{/if}
|
|
513
|
+
</div>
|
|
514
|
+
<div class="absolute -bottom-4 ml-3.5 flex w-full gap-1.5">
|
|
515
|
+
{#if alternatives.length > 1 && editMsdgId === null}
|
|
516
|
+
<Alternatives
|
|
517
|
+
{message}
|
|
518
|
+
{alternatives}
|
|
519
|
+
{loading}
|
|
520
|
+
onshowAlternateMsg={(payload) => onshowAlternateMsg?.(payload)}
|
|
521
|
+
/>
|
|
522
|
+
{/if}
|
|
523
|
+
{#if (alternatives.length > 1 && editMsdgId === null) || (!loading && !editMode)}
|
|
524
|
+
<button
|
|
525
|
+
class="hidden cursor-pointer items-center gap-1 rounded-md border border-gray-200 px-1.5 py-0.5 text-xs text-gray-400 group-hover:flex hover:flex hover:text-gray-500 dark:border-gray-700 dark:text-gray-400 dark:hover:text-gray-300 lg:-right-2"
|
|
526
|
+
title="Edit"
|
|
527
|
+
type="button"
|
|
528
|
+
onclick={() => {
|
|
529
|
+
if (requireAuthUser()) return;
|
|
530
|
+
editMsdgId = message.id;
|
|
531
|
+
}}
|
|
532
|
+
>
|
|
533
|
+
<CarbonPen />
|
|
534
|
+
Edit
|
|
535
|
+
</button>
|
|
536
|
+
{/if}
|
|
537
|
+
</div>
|
|
538
|
+
</div>
|
|
539
|
+
</div>
|
|
540
|
+
{/if}
|
|
541
|
+
|
|
542
|
+
<style>
|
|
543
|
+
@keyframes loading {
|
|
544
|
+
to {
|
|
545
|
+
stroke-dashoffset: 122.9;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
</style>
|