@tonyclaw/llm-inspector 1.6.0
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/.output/nitro.json +17 -0
- package/.output/public/assets/alibaba-TTwafVwX.svg +1 -0
- package/.output/public/assets/index-B3RwBPLW.css +1 -0
- package/.output/public/assets/index-s4lwsWvq.js +97 -0
- package/.output/public/assets/main-Cp8AM0Pa.js +17 -0
- package/.output/public/assets/minimax-BPMzvuL-.jpeg +0 -0
- package/.output/public/assets/qwen-CONDcHqt.png +0 -0
- package/.output/public/assets/zhipuai-BPNAnxo-.svg +219 -0
- package/.output/server/_chunks/ssr-renderer.mjs +17 -0
- package/.output/server/_libs/@radix-ui/react-accessible-icon+[...].mjs +1 -0
- package/.output/server/_libs/@radix-ui/react-dismissable-layer+[...].mjs +210 -0
- package/.output/server/_libs/@radix-ui/react-navigation-menu+[...].mjs +1 -0
- package/.output/server/_libs/@radix-ui/react-one-time-password-field+[...].mjs +1 -0
- package/.output/server/_libs/@radix-ui/react-password-toggle-field+[...].mjs +1 -0
- package/.output/server/_libs/@radix-ui/react-use-callback-ref+[...].mjs +11 -0
- package/.output/server/_libs/@radix-ui/react-use-controllable-state+[...].mjs +69 -0
- package/.output/server/_libs/@radix-ui/react-use-effect-event+[...].mjs +1 -0
- package/.output/server/_libs/@radix-ui/react-use-escape-keydown+[...].mjs +17 -0
- package/.output/server/_libs/@radix-ui/react-use-is-hydrated+[...].mjs +1 -0
- package/.output/server/_libs/@radix-ui/react-use-layout-effect+[...].mjs +6 -0
- package/.output/server/_libs/@radix-ui/react-visually-hidden+[...].mjs +34 -0
- package/.output/server/_libs/ajv-formats.mjs +330 -0
- package/.output/server/_libs/ajv.mjs +11444 -0
- package/.output/server/_libs/aria-hidden.mjs +122 -0
- package/.output/server/_libs/atomically.mjs +152 -0
- package/.output/server/_libs/bail.mjs +8 -0
- package/.output/server/_libs/character-entities.mjs +2130 -0
- package/.output/server/_libs/class-variance-authority.mjs +44 -0
- package/.output/server/_libs/clsx.mjs +16 -0
- package/.output/server/_libs/comma-separated-tokens.mjs +10 -0
- package/.output/server/_libs/conf.mjs +635 -0
- package/.output/server/_libs/cookie-es.mjs +58 -0
- package/.output/server/_libs/core-util-is.mjs +75 -0
- package/.output/server/_libs/croner.mjs +1 -0
- package/.output/server/_libs/crossws.mjs +1 -0
- package/.output/server/_libs/debounce-fn.mjs +69 -0
- package/.output/server/_libs/decode-named-character-reference+[...].mjs +8 -0
- package/.output/server/_libs/detect-node-es.mjs +1 -0
- package/.output/server/_libs/devlop.mjs +8 -0
- package/.output/server/_libs/dot-prop.mjs +265 -0
- package/.output/server/_libs/env-paths.mjs +57 -0
- package/.output/server/_libs/estree-util-is-identifier-name.mjs +11 -0
- package/.output/server/_libs/extend.mjs +97 -0
- package/.output/server/_libs/fast-deep-equal.mjs +38 -0
- package/.output/server/_libs/fast-uri.mjs +812 -0
- package/.output/server/_libs/floating-ui__core.mjs +725 -0
- package/.output/server/_libs/floating-ui__dom.mjs +622 -0
- package/.output/server/_libs/floating-ui__react-dom.mjs +292 -0
- package/.output/server/_libs/floating-ui__utils.mjs +320 -0
- package/.output/server/_libs/get-nonce.mjs +9 -0
- package/.output/server/_libs/h3-v2.mjs +276 -0
- package/.output/server/_libs/h3.mjs +400 -0
- package/.output/server/_libs/hast-util-to-jsx-runtime.mjs +388 -0
- package/.output/server/_libs/hast-util-whitespace.mjs +10 -0
- package/.output/server/_libs/hookable.mjs +1 -0
- package/.output/server/_libs/html-url-attributes.mjs +26 -0
- package/.output/server/_libs/immediate.mjs +74 -0
- package/.output/server/_libs/inherits.mjs +50 -0
- package/.output/server/_libs/inline-style-parser.mjs +142 -0
- package/.output/server/_libs/is-plain-obj.mjs +10 -0
- package/.output/server/_libs/isarray.mjs +14 -0
- package/.output/server/_libs/isbot.mjs +20 -0
- package/.output/server/_libs/json-schema-traverse.mjs +180 -0
- package/.output/server/_libs/jszip.mjs +3049 -0
- package/.output/server/_libs/lie.mjs +273 -0
- package/.output/server/_libs/lucide-react.mjs +368 -0
- package/.output/server/_libs/mdast-util-from-markdown.mjs +717 -0
- package/.output/server/_libs/mdast-util-to-hast.mjs +710 -0
- package/.output/server/_libs/mdast-util-to-string.mjs +38 -0
- package/.output/server/_libs/micromark-core-commonmark.mjs +2259 -0
- package/.output/server/_libs/micromark-factory-destination.mjs +94 -0
- package/.output/server/_libs/micromark-factory-label.mjs +63 -0
- package/.output/server/_libs/micromark-factory-space.mjs +24 -0
- package/.output/server/_libs/micromark-factory-title.mjs +65 -0
- package/.output/server/_libs/micromark-factory-whitespace.mjs +22 -0
- package/.output/server/_libs/micromark-util-character.mjs +44 -0
- package/.output/server/_libs/micromark-util-chunked.mjs +36 -0
- package/.output/server/_libs/micromark-util-classify-character+[...].mjs +12 -0
- package/.output/server/_libs/micromark-util-combine-extensions+[...].mjs +41 -0
- package/.output/server/_libs/micromark-util-decode-numeric-character-reference+[...].mjs +19 -0
- package/.output/server/_libs/micromark-util-decode-string.mjs +21 -0
- package/.output/server/_libs/micromark-util-encode.mjs +1 -0
- package/.output/server/_libs/micromark-util-html-tag-name.mjs +69 -0
- package/.output/server/_libs/micromark-util-normalize-identifier+[...].mjs +6 -0
- package/.output/server/_libs/micromark-util-resolve-all.mjs +15 -0
- package/.output/server/_libs/micromark-util-sanitize-uri.mjs +41 -0
- package/.output/server/_libs/micromark-util-subtokenize.mjs +346 -0
- package/.output/server/_libs/micromark.mjs +906 -0
- package/.output/server/_libs/mimic-function.mjs +47 -0
- package/.output/server/_libs/ohash.mjs +1 -0
- package/.output/server/_libs/pako.mjs +4223 -0
- package/.output/server/_libs/process-nextick-args.mjs +48 -0
- package/.output/server/_libs/property-information.mjs +1209 -0
- package/.output/server/_libs/radix-ui.mjs +1 -0
- package/.output/server/_libs/radix-ui__number.mjs +6 -0
- package/.output/server/_libs/radix-ui__primitive.mjs +11 -0
- package/.output/server/_libs/radix-ui__react-accordion.mjs +1 -0
- package/.output/server/_libs/radix-ui__react-alert-dialog.mjs +1 -0
- package/.output/server/_libs/radix-ui__react-arrow.mjs +23 -0
- package/.output/server/_libs/radix-ui__react-aspect-ratio.mjs +1 -0
- package/.output/server/_libs/radix-ui__react-avatar.mjs +1 -0
- package/.output/server/_libs/radix-ui__react-checkbox.mjs +1 -0
- package/.output/server/_libs/radix-ui__react-collapsible.mjs +144 -0
- package/.output/server/_libs/radix-ui__react-collection.mjs +69 -0
- package/.output/server/_libs/radix-ui__react-compose-refs.mjs +39 -0
- package/.output/server/_libs/radix-ui__react-context-menu.mjs +1 -0
- package/.output/server/_libs/radix-ui__react-context.mjs +78 -0
- package/.output/server/_libs/radix-ui__react-dialog.mjs +325 -0
- package/.output/server/_libs/radix-ui__react-direction.mjs +9 -0
- package/.output/server/_libs/radix-ui__react-dropdown-menu.mjs +1 -0
- package/.output/server/_libs/radix-ui__react-focus-guards.mjs +29 -0
- package/.output/server/_libs/radix-ui__react-focus-scope.mjs +206 -0
- package/.output/server/_libs/radix-ui__react-form.mjs +1 -0
- package/.output/server/_libs/radix-ui__react-hover-card.mjs +1 -0
- package/.output/server/_libs/radix-ui__react-id.mjs +14 -0
- package/.output/server/_libs/radix-ui__react-label.mjs +1 -0
- package/.output/server/_libs/radix-ui__react-menu.mjs +1 -0
- package/.output/server/_libs/radix-ui__react-menubar.mjs +1 -0
- package/.output/server/_libs/radix-ui__react-popover.mjs +1 -0
- package/.output/server/_libs/radix-ui__react-popper.mjs +286 -0
- package/.output/server/_libs/radix-ui__react-portal.mjs +16 -0
- package/.output/server/_libs/radix-ui__react-presence.mjs +128 -0
- package/.output/server/_libs/radix-ui__react-primitive.mjs +42 -0
- package/.output/server/_libs/radix-ui__react-progress.mjs +1 -0
- package/.output/server/_libs/radix-ui__react-radio-group.mjs +1 -0
- package/.output/server/_libs/radix-ui__react-roving-focus.mjs +224 -0
- package/.output/server/_libs/radix-ui__react-scroll-area.mjs +721 -0
- package/.output/server/_libs/radix-ui__react-select.mjs +1163 -0
- package/.output/server/_libs/radix-ui__react-separator.mjs +28 -0
- package/.output/server/_libs/radix-ui__react-slider.mjs +1 -0
- package/.output/server/_libs/radix-ui__react-slot.mjs +99 -0
- package/.output/server/_libs/radix-ui__react-switch.mjs +1 -0
- package/.output/server/_libs/radix-ui__react-tabs.mjs +189 -0
- package/.output/server/_libs/radix-ui__react-toast.mjs +1 -0
- package/.output/server/_libs/radix-ui__react-toggle-group.mjs +1 -0
- package/.output/server/_libs/radix-ui__react-toggle.mjs +1 -0
- package/.output/server/_libs/radix-ui__react-toolbar.mjs +1 -0
- package/.output/server/_libs/radix-ui__react-tooltip.mjs +495 -0
- package/.output/server/_libs/radix-ui__react-use-previous.mjs +14 -0
- package/.output/server/_libs/radix-ui__react-use-size.mjs +39 -0
- package/.output/server/_libs/react-dom.mjs +9935 -0
- package/.output/server/_libs/react-markdown.mjs +147 -0
- package/.output/server/_libs/react-remove-scroll-bar.mjs +82 -0
- package/.output/server/_libs/react-remove-scroll.mjs +328 -0
- package/.output/server/_libs/react-style-singleton.mjs +69 -0
- package/.output/server/_libs/react.mjs +515 -0
- package/.output/server/_libs/readable-stream.mjs +1518 -0
- package/.output/server/_libs/remark-parse.mjs +19 -0
- package/.output/server/_libs/remark-rehype.mjs +21 -0
- package/.output/server/_libs/rou3.mjs +8 -0
- package/.output/server/_libs/safe-buffer.mjs +64 -0
- package/.output/server/_libs/semver.mjs +1984 -0
- package/.output/server/_libs/seroval-plugins.mjs +58 -0
- package/.output/server/_libs/seroval.mjs +1765 -0
- package/.output/server/_libs/setimmediate.mjs +1 -0
- package/.output/server/_libs/space-separated-tokens.mjs +6 -0
- package/.output/server/_libs/srvx.mjs +334 -0
- package/.output/server/_libs/stubborn-fs.mjs +91 -0
- package/.output/server/_libs/stubborn-utils.mjs +66 -0
- package/.output/server/_libs/style-to-js.mjs +72 -0
- package/.output/server/_libs/style-to-object.mjs +38 -0
- package/.output/server/_libs/tailwind-merge.mjs +3010 -0
- package/.output/server/_libs/tanstack__history.mjs +217 -0
- package/.output/server/_libs/tanstack__react-router.mjs +1480 -0
- package/.output/server/_libs/tanstack__react-store.mjs +1 -0
- package/.output/server/_libs/tanstack__react-virtual.mjs +44 -0
- package/.output/server/_libs/tanstack__router-core.mjs +4827 -0
- package/.output/server/_libs/tanstack__store.mjs +1 -0
- package/.output/server/_libs/tanstack__virtual-core.mjs +1225 -0
- package/.output/server/_libs/tiny-invariant.mjs +12 -0
- package/.output/server/_libs/tiny-warning.mjs +5 -0
- package/.output/server/_libs/trim-lines.mjs +41 -0
- package/.output/server/_libs/trough.mjs +85 -0
- package/.output/server/_libs/tslib.mjs +576 -0
- package/.output/server/_libs/ufo.mjs +54 -0
- package/.output/server/_libs/uint8array-extras.mjs +69 -0
- package/.output/server/_libs/unctx.mjs +1 -0
- package/.output/server/_libs/ungap__structured-clone.mjs +212 -0
- package/.output/server/_libs/unified.mjs +661 -0
- package/.output/server/_libs/unist-util-is.mjs +100 -0
- package/.output/server/_libs/unist-util-position.mjs +27 -0
- package/.output/server/_libs/unist-util-stringify-position.mjs +27 -0
- package/.output/server/_libs/unist-util-visit-parents.mjs +82 -0
- package/.output/server/_libs/unist-util-visit.mjs +24 -0
- package/.output/server/_libs/unstorage.mjs +1 -0
- package/.output/server/_libs/use-callback-ref.mjs +66 -0
- package/.output/server/_libs/use-sidecar.mjs +106 -0
- package/.output/server/_libs/use-sync-external-store.mjs +1 -0
- package/.output/server/_libs/util-deprecate.mjs +12 -0
- package/.output/server/_libs/vfile-message.mjs +138 -0
- package/.output/server/_libs/vfile.mjs +467 -0
- package/.output/server/_libs/when-exit.mjs +53 -0
- package/.output/server/_libs/zod.mjs +4460 -0
- package/.output/server/_ssr/index-ByCLZu7J.mjs +3061 -0
- package/.output/server/_ssr/index.mjs +1176 -0
- package/.output/server/_ssr/router-Bq_mxeNz.mjs +2872 -0
- package/.output/server/_ssr/start-HYkvq4Ni.mjs +4 -0
- package/.output/server/_tanstack-start-manifest_v-C4E0e9my.mjs +4 -0
- package/.output/server/index.mjs +393 -0
- package/README.md +196 -0
- package/package.json +91 -0
- package/src/assets/logos/alibaba.svg +1 -0
- package/src/assets/logos/anthropic.svg +1 -0
- package/src/assets/logos/deepseek.svg +1 -0
- package/src/assets/logos/minimax.jpeg +0 -0
- package/src/assets/logos/openai.svg +1 -0
- package/src/assets/logos/qwen.png +0 -0
- package/src/assets/logos/zhipuai.svg +219 -0
- package/src/cli.ts +68 -0
- package/src/components/ProxyViewer.tsx +325 -0
- package/src/components/ProxyViewerContainer.tsx +211 -0
- package/src/components/providers/ProviderCard.tsx +186 -0
- package/src/components/providers/ProviderForm.tsx +259 -0
- package/src/components/providers/ProviderLogo.tsx +111 -0
- package/src/components/providers/ProvidersPanel.tsx +259 -0
- package/src/components/providers/SettingsDialog.tsx +39 -0
- package/src/components/proxy-viewer/ConversationGroup.tsx +68 -0
- package/src/components/proxy-viewer/ConversationHeader.tsx +141 -0
- package/src/components/proxy-viewer/LogEntry.tsx +225 -0
- package/src/components/proxy-viewer/LogEntryHeader.tsx +250 -0
- package/src/components/proxy-viewer/ReplayDialog.tsx +208 -0
- package/src/components/proxy-viewer/ResponseView.tsx +161 -0
- package/src/components/proxy-viewer/StreamingChunkSequence.tsx +171 -0
- package/src/components/proxy-viewer/formats/anthropic/ContentBlocks.tsx +139 -0
- package/src/components/proxy-viewer/formats/anthropic/ResponseView.tsx +64 -0
- package/src/components/proxy-viewer/formats/index.tsx +24 -0
- package/src/components/proxy-viewer/formats/openai/ResponseView.tsx +80 -0
- package/src/components/proxy-viewer/index.ts +8 -0
- package/src/components/ui/badge.tsx +47 -0
- package/src/components/ui/button.tsx +47 -0
- package/src/components/ui/collapsible.tsx +21 -0
- package/src/components/ui/dialog.tsx +129 -0
- package/src/components/ui/json-viewer.tsx +464 -0
- package/src/components/ui/scroll-area.tsx +54 -0
- package/src/components/ui/select.tsx +178 -0
- package/src/components/ui/separator.tsx +28 -0
- package/src/components/ui/tabs.tsx +88 -0
- package/src/components/ui/tooltip.tsx +51 -0
- package/src/index.css +11 -0
- package/src/lib/export-logs.ts +51 -0
- package/src/lib/utils.ts +22 -0
- package/src/proxy/chunkStorage.ts +118 -0
- package/src/proxy/constants.ts +36 -0
- package/src/proxy/formats/anthropic/anthropicProvider.ts +75 -0
- package/src/proxy/formats/anthropic/handler.ts +74 -0
- package/src/proxy/formats/anthropic/index.ts +14 -0
- package/src/proxy/formats/anthropic/register.ts +4 -0
- package/src/proxy/formats/anthropic/schemas.ts +217 -0
- package/src/proxy/formats/anthropic/stream.ts +167 -0
- package/src/proxy/formats/handler.ts +46 -0
- package/src/proxy/formats/index.ts +12 -0
- package/src/proxy/formats/jsonSchema.ts +24 -0
- package/src/proxy/formats/openai/alibabaProvider.ts +38 -0
- package/src/proxy/formats/openai/handler.ts +70 -0
- package/src/proxy/formats/openai/index.ts +25 -0
- package/src/proxy/formats/openai/provider.ts +50 -0
- package/src/proxy/formats/openai/register.ts +4 -0
- package/src/proxy/formats/openai/schemas.ts +150 -0
- package/src/proxy/formats/openai/stream.ts +153 -0
- package/src/proxy/formats/protocol.ts +50 -0
- package/src/proxy/formats/providerRegistry.ts +51 -0
- package/src/proxy/formats/providers/index.ts +3 -0
- package/src/proxy/formats/registry.ts +61 -0
- package/src/proxy/handler.ts +389 -0
- package/src/proxy/logIndex.ts +187 -0
- package/src/proxy/logger.ts +99 -0
- package/src/proxy/providers.ts +234 -0
- package/src/proxy/schemas.ts +160 -0
- package/src/proxy/socketTracker.ts +158 -0
- package/src/proxy/store.ts +386 -0
- package/src/router.tsx +16 -0
- package/src/routes/__root.tsx +38 -0
- package/src/routes/api/config.paths.ts +14 -0
- package/src/routes/api/health.ts +11 -0
- package/src/routes/api/logs.$id.chunks.ts +36 -0
- package/src/routes/api/logs.$id.replay.ts +262 -0
- package/src/routes/api/logs.$id.ts +22 -0
- package/src/routes/api/logs.stream.ts +64 -0
- package/src/routes/api/logs.ts +30 -0
- package/src/routes/api/models.ts +10 -0
- package/src/routes/api/providers.$providerId.ts +45 -0
- package/src/routes/api/providers.ts +37 -0
- package/src/routes/api/sessions.ts +10 -0
- package/src/routes/index.tsx +6 -0
- package/src/routes/proxy/$.ts +15 -0
- package/styles/globals.css +121 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import type { JSX } from "react";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import AnthropicLogoSvg from "../../assets/logos/anthropic.svg";
|
|
4
|
+
import OpenAILogoSvg from "../../assets/logos/openai.svg";
|
|
5
|
+
import DeepSeekLogoSvg from "../../assets/logos/deepseek.svg";
|
|
6
|
+
import MiniMaxLogoSvg from "../../assets/logos/minimax.jpeg";
|
|
7
|
+
import AlibabaLogoSvg from "../../assets/logos/alibaba.svg";
|
|
8
|
+
import QwenLogoSvg from "../../assets/logos/qwen.png";
|
|
9
|
+
import ZhipuAILogoSvg from "../../assets/logos/zhipuai.svg";
|
|
10
|
+
|
|
11
|
+
export type Provider =
|
|
12
|
+
| "anthropic"
|
|
13
|
+
| "openai"
|
|
14
|
+
| "deepseek"
|
|
15
|
+
| "minimax"
|
|
16
|
+
| "alibaba"
|
|
17
|
+
| "qwen"
|
|
18
|
+
| "zhipuai"
|
|
19
|
+
| "unknown";
|
|
20
|
+
|
|
21
|
+
const PROVIDER_MAP: Record<string, Provider> = {
|
|
22
|
+
"claude-": "anthropic",
|
|
23
|
+
"gpt-": "openai",
|
|
24
|
+
"o1-": "openai",
|
|
25
|
+
"o3-": "openai",
|
|
26
|
+
"deepseek-": "deepseek",
|
|
27
|
+
MiniMax: "minimax",
|
|
28
|
+
qwen: "qwen",
|
|
29
|
+
"glm-": "zhipuai",
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export function detectProvider(model: string | null): Provider {
|
|
33
|
+
if (model === null) return "unknown";
|
|
34
|
+
for (const [prefix, provider] of Object.entries(PROVIDER_MAP)) {
|
|
35
|
+
if (model.startsWith(prefix)) {
|
|
36
|
+
return provider;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return "unknown";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const sizeStyle = { width: 24, height: 24, objectFit: "contain" as const };
|
|
43
|
+
|
|
44
|
+
const AnthropicLogo = React.memo(
|
|
45
|
+
({ className }: { className?: string }): JSX.Element => (
|
|
46
|
+
<img src={AnthropicLogoSvg} alt="Anthropic" className={className} style={sizeStyle} />
|
|
47
|
+
),
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
const OpenAILogo = React.memo(
|
|
51
|
+
({ className }: { className?: string }): JSX.Element => (
|
|
52
|
+
<img src={OpenAILogoSvg} alt="OpenAI" className={className} style={sizeStyle} />
|
|
53
|
+
),
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const DeepSeekLogo = React.memo(
|
|
57
|
+
({ className }: { className?: string }): JSX.Element => (
|
|
58
|
+
<img src={DeepSeekLogoSvg} alt="DeepSeek" className={className} style={sizeStyle} />
|
|
59
|
+
),
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
const MiniMaxLogo = React.memo(
|
|
63
|
+
({ className }: { className?: string }): JSX.Element => (
|
|
64
|
+
<img src={MiniMaxLogoSvg} alt="MiniMax" className={className} style={sizeStyle} />
|
|
65
|
+
),
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
const AlibabaLogo = React.memo(
|
|
69
|
+
({ className }: { className?: string }): JSX.Element => (
|
|
70
|
+
<img src={AlibabaLogoSvg} alt="Alibaba" className={className} style={sizeStyle} />
|
|
71
|
+
),
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const QwenLogo = React.memo(
|
|
75
|
+
({ className }: { className?: string }): JSX.Element => (
|
|
76
|
+
<img src={QwenLogoSvg} alt="Qwen" className={className} style={sizeStyle} />
|
|
77
|
+
),
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
const ZhipuAILogo = React.memo(
|
|
81
|
+
({ className }: { className?: string }): JSX.Element => (
|
|
82
|
+
<img src={ZhipuAILogoSvg} alt="ZhipuAI" className={className} style={sizeStyle} />
|
|
83
|
+
),
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
export function ProviderLogo({
|
|
87
|
+
provider,
|
|
88
|
+
className,
|
|
89
|
+
}: {
|
|
90
|
+
provider: Provider;
|
|
91
|
+
className?: string;
|
|
92
|
+
}): JSX.Element | null {
|
|
93
|
+
switch (provider) {
|
|
94
|
+
case "anthropic":
|
|
95
|
+
return <AnthropicLogo className={className} />;
|
|
96
|
+
case "openai":
|
|
97
|
+
return <OpenAILogo className={className} />;
|
|
98
|
+
case "deepseek":
|
|
99
|
+
return <DeepSeekLogo className={className} />;
|
|
100
|
+
case "minimax":
|
|
101
|
+
return <MiniMaxLogo className={className} />;
|
|
102
|
+
case "alibaba":
|
|
103
|
+
return <AlibabaLogo className={className} />;
|
|
104
|
+
case "qwen":
|
|
105
|
+
return <QwenLogo className={className} />;
|
|
106
|
+
case "zhipuai":
|
|
107
|
+
return <ZhipuAILogo className={className} />;
|
|
108
|
+
case "unknown":
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { type JSX, useState, useEffect, useCallback } from "react";
|
|
2
|
+
import { Button } from "../ui/button";
|
|
3
|
+
import { Plus, AlertCircle, Copy, Check } from "lucide-react";
|
|
4
|
+
import { ProviderCard } from "./ProviderCard";
|
|
5
|
+
import { ProviderForm } from "./ProviderForm";
|
|
6
|
+
import type { ProviderConfig } from "../../proxy/providers";
|
|
7
|
+
|
|
8
|
+
type ConfigPathsResponse = {
|
|
9
|
+
providerConfig: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
type TestResult = {
|
|
13
|
+
success: boolean;
|
|
14
|
+
error?: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
type NotConfigured = { notConfigured: true };
|
|
18
|
+
|
|
19
|
+
type StreamingTestResults = {
|
|
20
|
+
nonStreaming: TestResult | NotConfigured;
|
|
21
|
+
streaming: TestResult | NotConfigured;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
type TestResults = {
|
|
25
|
+
anthropic: StreamingTestResults;
|
|
26
|
+
openai: StreamingTestResults;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export function ProvidersPanel(): JSX.Element {
|
|
30
|
+
const [providers, setProviders] = useState<ProviderConfig[]>([]);
|
|
31
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
32
|
+
const [showForm, setShowForm] = useState(false);
|
|
33
|
+
const [editingProvider, setEditingProvider] = useState<ProviderConfig | undefined>();
|
|
34
|
+
const [error, setError] = useState<string | null>(null);
|
|
35
|
+
const [testResults, setTestResults] = useState<Record<string, TestResults>>({});
|
|
36
|
+
const [testingProviders, setTestingProviders] = useState<Set<string>>(new Set());
|
|
37
|
+
const [configPath, setConfigPath] = useState<string | null>(null);
|
|
38
|
+
const [configPathCopied, setConfigPathCopied] = useState(false);
|
|
39
|
+
|
|
40
|
+
const fetchProviders = useCallback(async (): Promise<void> => {
|
|
41
|
+
try {
|
|
42
|
+
const providersRes = await fetch("/api/providers");
|
|
43
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
44
|
+
const providersData = (await providersRes.json()) as ProviderConfig[];
|
|
45
|
+
setProviders(providersData);
|
|
46
|
+
} catch {
|
|
47
|
+
setError("Failed to load providers");
|
|
48
|
+
} finally {
|
|
49
|
+
setIsLoading(false);
|
|
50
|
+
}
|
|
51
|
+
}, []);
|
|
52
|
+
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
void fetchProviders();
|
|
55
|
+
void (async () => {
|
|
56
|
+
try {
|
|
57
|
+
const res = await fetch("/api/config/paths");
|
|
58
|
+
if (res.ok) {
|
|
59
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
60
|
+
const data = (await res.json()) as ConfigPathsResponse;
|
|
61
|
+
setConfigPath(data.providerConfig);
|
|
62
|
+
}
|
|
63
|
+
} catch {
|
|
64
|
+
// Ignore
|
|
65
|
+
}
|
|
66
|
+
})();
|
|
67
|
+
}, [fetchProviders]);
|
|
68
|
+
|
|
69
|
+
const runTest = useCallback(async (providerId: string): Promise<void> => {
|
|
70
|
+
setTestingProviders((prev) => new Set(prev).add(providerId));
|
|
71
|
+
try {
|
|
72
|
+
const res = await fetch(`/api/providers/${providerId}/test`, { method: "POST" });
|
|
73
|
+
if (res.ok) {
|
|
74
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
75
|
+
const results = (await res.json()) as TestResults;
|
|
76
|
+
setTestResults((prev) => ({ ...prev, [providerId]: results }));
|
|
77
|
+
}
|
|
78
|
+
} finally {
|
|
79
|
+
setTestingProviders((prev) => {
|
|
80
|
+
const next = new Set(prev);
|
|
81
|
+
next.delete(providerId);
|
|
82
|
+
return next;
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}, []);
|
|
86
|
+
|
|
87
|
+
function handleAddProvider(data: {
|
|
88
|
+
name: string;
|
|
89
|
+
apiKey: string;
|
|
90
|
+
model?: string;
|
|
91
|
+
anthropicBaseUrl?: string;
|
|
92
|
+
openaiBaseUrl?: string;
|
|
93
|
+
}): void {
|
|
94
|
+
void (async () => {
|
|
95
|
+
const res = await fetch("/api/providers", {
|
|
96
|
+
method: "POST",
|
|
97
|
+
headers: { "Content-Type": "application/json" },
|
|
98
|
+
body: JSON.stringify(data),
|
|
99
|
+
});
|
|
100
|
+
if (!res.ok) {
|
|
101
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
102
|
+
const err = (await res.json()) as { error?: string };
|
|
103
|
+
setError(err.error ?? "Failed to add provider");
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
107
|
+
const newProvider = (await res.json()) as ProviderConfig;
|
|
108
|
+
await fetchProviders();
|
|
109
|
+
setShowForm(false);
|
|
110
|
+
// Run test on new provider
|
|
111
|
+
await runTest(newProvider.id);
|
|
112
|
+
})();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function handleUpdateProvider(data: {
|
|
116
|
+
name: string;
|
|
117
|
+
apiKey: string;
|
|
118
|
+
model?: string;
|
|
119
|
+
anthropicBaseUrl?: string;
|
|
120
|
+
openaiBaseUrl?: string;
|
|
121
|
+
}): void {
|
|
122
|
+
if (!editingProvider) return;
|
|
123
|
+
void (async () => {
|
|
124
|
+
const res = await fetch(`/api/providers/${editingProvider.id}`, {
|
|
125
|
+
method: "PUT",
|
|
126
|
+
headers: { "Content-Type": "application/json" },
|
|
127
|
+
body: JSON.stringify(data),
|
|
128
|
+
});
|
|
129
|
+
if (!res.ok) {
|
|
130
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
131
|
+
const err = (await res.json()) as { error?: string };
|
|
132
|
+
setError(err.error ?? "Failed to update provider");
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
136
|
+
const updated = (await res.json()) as ProviderConfig;
|
|
137
|
+
await fetchProviders();
|
|
138
|
+
setEditingProvider(undefined);
|
|
139
|
+
// Run test on updated provider
|
|
140
|
+
await runTest(updated.id);
|
|
141
|
+
})();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function handleDeleteProvider(providerId: string): void {
|
|
145
|
+
// eslint-disable-next-line no-alert
|
|
146
|
+
if (!window.confirm("Are you sure you want to delete this provider?")) return;
|
|
147
|
+
void (async () => {
|
|
148
|
+
const res = await fetch(`/api/providers/${providerId}`, {
|
|
149
|
+
method: "DELETE",
|
|
150
|
+
});
|
|
151
|
+
if (!res.ok) {
|
|
152
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
153
|
+
const err = (await res.json()) as { error?: string };
|
|
154
|
+
setError(err.error ?? "Failed to delete provider");
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
await fetchProviders();
|
|
158
|
+
})();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (isLoading) {
|
|
162
|
+
return (
|
|
163
|
+
<div className="flex items-center justify-center py-8">
|
|
164
|
+
<p className="text-sm text-muted-foreground">Loading providers...</p>
|
|
165
|
+
</div>
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (showForm || editingProvider) {
|
|
170
|
+
return (
|
|
171
|
+
<div className="space-y-4">
|
|
172
|
+
<div className="flex items-center justify-between">
|
|
173
|
+
<h3 className="text-lg font-medium">
|
|
174
|
+
{editingProvider ? "Edit Provider" : "Add New Provider"}
|
|
175
|
+
</h3>
|
|
176
|
+
</div>
|
|
177
|
+
<ProviderForm
|
|
178
|
+
provider={editingProvider}
|
|
179
|
+
onSubmit={editingProvider ? handleUpdateProvider : handleAddProvider}
|
|
180
|
+
onCancel={() => {
|
|
181
|
+
setShowForm(false);
|
|
182
|
+
setEditingProvider(undefined);
|
|
183
|
+
}}
|
|
184
|
+
/>
|
|
185
|
+
</div>
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return (
|
|
190
|
+
<div className="space-y-4">
|
|
191
|
+
<div className="flex items-center justify-between">
|
|
192
|
+
<h3 className="text-lg font-medium">Providers</h3>
|
|
193
|
+
<Button onClick={() => setShowForm(true)} size="sm" className="gap-1">
|
|
194
|
+
<Plus className="size-4" />
|
|
195
|
+
Add Provider
|
|
196
|
+
</Button>
|
|
197
|
+
</div>
|
|
198
|
+
|
|
199
|
+
{configPath !== null && (
|
|
200
|
+
<div className="flex items-center gap-2 text-xs text-muted-foreground bg-muted/30 rounded-md px-3 py-2">
|
|
201
|
+
<span className="shrink-0">Config:</span>
|
|
202
|
+
<span className="font-mono truncate" title={configPath}>
|
|
203
|
+
{configPath}
|
|
204
|
+
</span>
|
|
205
|
+
<button
|
|
206
|
+
type="button"
|
|
207
|
+
onClick={() => {
|
|
208
|
+
void window.navigator.clipboard.writeText(configPath).then(() => {
|
|
209
|
+
setConfigPathCopied(true);
|
|
210
|
+
setTimeout(() => setConfigPathCopied(false), 2000);
|
|
211
|
+
});
|
|
212
|
+
}}
|
|
213
|
+
className="shrink-0 ml-auto text-muted-foreground hover:text-foreground transition-colors"
|
|
214
|
+
title="Copy path"
|
|
215
|
+
>
|
|
216
|
+
{configPathCopied ? (
|
|
217
|
+
<Check className="size-3 text-green-500" />
|
|
218
|
+
) : (
|
|
219
|
+
<Copy className="size-3" />
|
|
220
|
+
)}
|
|
221
|
+
</button>
|
|
222
|
+
</div>
|
|
223
|
+
)}
|
|
224
|
+
|
|
225
|
+
{error !== null && (
|
|
226
|
+
<div className="flex items-center gap-2 text-sm text-destructive bg-destructive/10 rounded-md px-3 py-2">
|
|
227
|
+
<AlertCircle className="size-4 shrink-0" />
|
|
228
|
+
{error}
|
|
229
|
+
</div>
|
|
230
|
+
)}
|
|
231
|
+
|
|
232
|
+
{providers.length === 0 ? (
|
|
233
|
+
<div className="text-center py-12 space-y-3">
|
|
234
|
+
<p className="text-sm text-muted-foreground">No providers configured yet.</p>
|
|
235
|
+
<Button onClick={() => setShowForm(true)} variant="outline" size="sm">
|
|
236
|
+
<Plus className="size-4" />
|
|
237
|
+
Add Your First Provider
|
|
238
|
+
</Button>
|
|
239
|
+
</div>
|
|
240
|
+
) : (
|
|
241
|
+
<div className="space-y-3">
|
|
242
|
+
{providers.map((provider) => (
|
|
243
|
+
<ProviderCard
|
|
244
|
+
key={provider.id}
|
|
245
|
+
provider={provider}
|
|
246
|
+
testResults={testResults[provider.id]}
|
|
247
|
+
isTesting={testingProviders.has(provider.id)}
|
|
248
|
+
onEdit={(p) => setEditingProvider(p)}
|
|
249
|
+
onDelete={handleDeleteProvider}
|
|
250
|
+
onTest={(id: string) => {
|
|
251
|
+
void runTest(id);
|
|
252
|
+
}}
|
|
253
|
+
/>
|
|
254
|
+
))}
|
|
255
|
+
</div>
|
|
256
|
+
)}
|
|
257
|
+
</div>
|
|
258
|
+
);
|
|
259
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { type JSX, useState } from "react";
|
|
2
|
+
import { Settings } from "lucide-react";
|
|
3
|
+
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "../ui/dialog";
|
|
4
|
+
import { Tabs, TabsList, TabsTrigger, TabsContent } from "../ui/tabs";
|
|
5
|
+
import { Button } from "../ui/button";
|
|
6
|
+
import { ProvidersPanel } from "./ProvidersPanel";
|
|
7
|
+
|
|
8
|
+
export function SettingsDialog(): JSX.Element {
|
|
9
|
+
const [open, setOpen] = useState(false);
|
|
10
|
+
const [activeTab, setActiveTab] = useState("providers");
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<Dialog open={open} onOpenChange={setOpen}>
|
|
14
|
+
<DialogTrigger asChild>
|
|
15
|
+
<Button variant="ghost" size="icon" className="size-8">
|
|
16
|
+
<Settings className="size-4" />
|
|
17
|
+
<span className="sr-only">Settings</span>
|
|
18
|
+
</Button>
|
|
19
|
+
</DialogTrigger>
|
|
20
|
+
<DialogContent className="max-w-2xl max-h-[80vh] overflow-hidden flex flex-col">
|
|
21
|
+
<DialogHeader>
|
|
22
|
+
<DialogTitle>Settings</DialogTitle>
|
|
23
|
+
</DialogHeader>
|
|
24
|
+
|
|
25
|
+
<Tabs value={activeTab} onValueChange={setActiveTab} className="flex-1 overflow-hidden">
|
|
26
|
+
<TabsList>
|
|
27
|
+
<TabsTrigger value="providers">Providers</TabsTrigger>
|
|
28
|
+
</TabsList>
|
|
29
|
+
|
|
30
|
+
<div className="mt-4 overflow-y-auto flex-1">
|
|
31
|
+
<TabsContent value="providers">
|
|
32
|
+
<ProvidersPanel />
|
|
33
|
+
</TabsContent>
|
|
34
|
+
</div>
|
|
35
|
+
</Tabs>
|
|
36
|
+
</DialogContent>
|
|
37
|
+
</Dialog>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import type { JSX } from "react";
|
|
3
|
+
import type { CapturedLog } from "../../proxy/schemas";
|
|
4
|
+
import {
|
|
5
|
+
ConversationHeader,
|
|
6
|
+
getConversationId,
|
|
7
|
+
type ConversationGroupData,
|
|
8
|
+
} from "./ConversationHeader";
|
|
9
|
+
import { LogEntry } from "./LogEntry";
|
|
10
|
+
|
|
11
|
+
export type ConversationGroupProps = {
|
|
12
|
+
group: ConversationGroupData;
|
|
13
|
+
viewMode?: "simple" | "full";
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
function computeStats(logs: CapturedLog[]): {
|
|
17
|
+
totalInputTokens: number;
|
|
18
|
+
totalOutputTokens: number;
|
|
19
|
+
} {
|
|
20
|
+
let totalInput = 0;
|
|
21
|
+
let totalOutput = 0;
|
|
22
|
+
for (const log of logs) {
|
|
23
|
+
if (log.inputTokens !== null) totalInput += log.inputTokens;
|
|
24
|
+
if (log.outputTokens !== null) totalOutput += log.outputTokens;
|
|
25
|
+
}
|
|
26
|
+
return { totalInputTokens: totalInput, totalOutputTokens: totalOutput };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function ConversationGroup({
|
|
30
|
+
group,
|
|
31
|
+
viewMode = "simple",
|
|
32
|
+
}: ConversationGroupProps): JSX.Element {
|
|
33
|
+
const [expanded, setExpanded] = useState(false);
|
|
34
|
+
|
|
35
|
+
const stats = computeStats(group.logs);
|
|
36
|
+
const startTime = group.logs[0]?.timestamp ?? new Date().toISOString();
|
|
37
|
+
const endTime = group.logs[group.logs.length - 1]?.timestamp ?? new Date().toISOString();
|
|
38
|
+
|
|
39
|
+
const displayId =
|
|
40
|
+
group.conversationId.startsWith("PID:") || group.conversationId.includes("|")
|
|
41
|
+
? group.conversationId
|
|
42
|
+
: group.conversationId.length > 24
|
|
43
|
+
? group.conversationId.slice(0, 12) + "…" + group.conversationId.slice(-12)
|
|
44
|
+
: group.conversationId;
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<div className="mb-4">
|
|
48
|
+
<ConversationHeader
|
|
49
|
+
conversationId={displayId}
|
|
50
|
+
startTime={startTime}
|
|
51
|
+
endTime={endTime}
|
|
52
|
+
totalCalls={group.logs.length}
|
|
53
|
+
totalInputTokens={stats.totalInputTokens}
|
|
54
|
+
totalOutputTokens={stats.totalOutputTokens}
|
|
55
|
+
expanded={expanded}
|
|
56
|
+
onToggle={() => setExpanded(!expanded)}
|
|
57
|
+
/>
|
|
58
|
+
|
|
59
|
+
{expanded && (
|
|
60
|
+
<div className="pl-4 border-l-2 border-muted ml-3">
|
|
61
|
+
{group.logs.map((log) => (
|
|
62
|
+
<LogEntry key={log.id} log={log} viewMode={viewMode} />
|
|
63
|
+
))}
|
|
64
|
+
</div>
|
|
65
|
+
)}
|
|
66
|
+
</div>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { ChevronDown, ChevronRight, Clock, MessageSquare, Zap } from "lucide-react";
|
|
2
|
+
import type { JSX } from "react";
|
|
3
|
+
import { cn, formatTokens } from "../../lib/utils";
|
|
4
|
+
import type { CapturedLog } from "../../proxy/schemas";
|
|
5
|
+
|
|
6
|
+
export type ConversationHeaderProps = {
|
|
7
|
+
conversationId: string;
|
|
8
|
+
startTime: string;
|
|
9
|
+
endTime: string;
|
|
10
|
+
totalCalls: number;
|
|
11
|
+
totalInputTokens: number;
|
|
12
|
+
totalOutputTokens: number;
|
|
13
|
+
expanded: boolean;
|
|
14
|
+
onToggle: () => void;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
function formatTimestamp(iso: string): string {
|
|
18
|
+
const date = new Date(iso);
|
|
19
|
+
return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function ConversationHeader({
|
|
23
|
+
conversationId,
|
|
24
|
+
startTime,
|
|
25
|
+
endTime,
|
|
26
|
+
totalCalls,
|
|
27
|
+
totalInputTokens,
|
|
28
|
+
totalOutputTokens,
|
|
29
|
+
expanded,
|
|
30
|
+
onToggle,
|
|
31
|
+
}: ConversationHeaderProps): JSX.Element {
|
|
32
|
+
return (
|
|
33
|
+
<div
|
|
34
|
+
role="button"
|
|
35
|
+
tabIndex={0}
|
|
36
|
+
className={cn(
|
|
37
|
+
"flex items-center gap-3 px-3 py-2 cursor-pointer transition-colors",
|
|
38
|
+
"hover:bg-muted/50",
|
|
39
|
+
"select-none",
|
|
40
|
+
"border border-border rounded-lg mb-2 bg-muted/30",
|
|
41
|
+
)}
|
|
42
|
+
onClick={onToggle}
|
|
43
|
+
onKeyDown={(e) => {
|
|
44
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
45
|
+
e.preventDefault();
|
|
46
|
+
onToggle();
|
|
47
|
+
}
|
|
48
|
+
}}
|
|
49
|
+
>
|
|
50
|
+
{/* Expand chevron */}
|
|
51
|
+
{expanded ? (
|
|
52
|
+
<ChevronDown className="size-4 text-muted-foreground shrink-0" />
|
|
53
|
+
) : (
|
|
54
|
+
<ChevronRight className="size-4 text-muted-foreground shrink-0" />
|
|
55
|
+
)}
|
|
56
|
+
|
|
57
|
+
{/* Conversation ID */}
|
|
58
|
+
<span
|
|
59
|
+
className="text-purple-400/90 font-mono text-xs font-semibold shrink-0"
|
|
60
|
+
title={conversationId}
|
|
61
|
+
>
|
|
62
|
+
{conversationId.length > 24
|
|
63
|
+
? conversationId.slice(0, 12) + "…" + conversationId.slice(-12)
|
|
64
|
+
: conversationId}
|
|
65
|
+
</span>
|
|
66
|
+
|
|
67
|
+
{/* Time range */}
|
|
68
|
+
<span className="flex items-center gap-1 text-muted-foreground text-xs shrink-0">
|
|
69
|
+
<Clock className="size-3" />
|
|
70
|
+
<span className="font-mono tabular-nums">
|
|
71
|
+
{formatTimestamp(startTime)} - {formatTimestamp(endTime)}
|
|
72
|
+
</span>
|
|
73
|
+
</span>
|
|
74
|
+
|
|
75
|
+
{/* Total calls */}
|
|
76
|
+
<span className="flex items-center gap-1 text-muted-foreground text-xs shrink-0">
|
|
77
|
+
<MessageSquare className="size-3" />
|
|
78
|
+
<span className="font-mono tabular-nums">
|
|
79
|
+
{totalCalls} call{totalCalls !== 1 ? "s" : ""}
|
|
80
|
+
</span>
|
|
81
|
+
</span>
|
|
82
|
+
|
|
83
|
+
{/* Token counts */}
|
|
84
|
+
<span className="flex items-center gap-1 text-xs shrink-0">
|
|
85
|
+
<Zap className="size-3 text-muted-foreground" />
|
|
86
|
+
<span className="font-mono tabular-nums">
|
|
87
|
+
<span className="text-blue-400">{formatTokens(totalInputTokens)}</span>
|
|
88
|
+
{" / "}
|
|
89
|
+
<span className="text-amber-400">{formatTokens(totalOutputTokens)}</span>
|
|
90
|
+
</span>
|
|
91
|
+
</span>
|
|
92
|
+
|
|
93
|
+
{/* Spacer */}
|
|
94
|
+
<span className="flex-1 min-w-0" />
|
|
95
|
+
</div>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export type ConversationGroupData = {
|
|
100
|
+
id: string;
|
|
101
|
+
conversationId: string;
|
|
102
|
+
logs: CapturedLog[];
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
export function getConversationId(log: CapturedLog): string {
|
|
106
|
+
if (log.sessionId !== null && log.sessionId !== "") {
|
|
107
|
+
return log.sessionId;
|
|
108
|
+
}
|
|
109
|
+
const pid = log.clientPid !== null ? `PID:${log.clientPid}` : "unknown";
|
|
110
|
+
const folder = log.clientProjectFolder !== null ? log.clientProjectFolder : "no-folder";
|
|
111
|
+
return `${pid}|${folder}`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function groupLogsByConversation(logs: CapturedLog[]): ConversationGroupData[] {
|
|
115
|
+
const groups = new Map<string, CapturedLog[]>();
|
|
116
|
+
|
|
117
|
+
for (const log of logs) {
|
|
118
|
+
const convId = getConversationId(log);
|
|
119
|
+
const existing = groups.get(convId);
|
|
120
|
+
if (existing !== undefined) {
|
|
121
|
+
existing.push(log);
|
|
122
|
+
} else {
|
|
123
|
+
groups.set(convId, [log]);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const result: ConversationGroupData[] = [];
|
|
128
|
+
|
|
129
|
+
for (const [conversationId, groupLogs] of groups) {
|
|
130
|
+
const sorted = [...groupLogs].sort((a, b) => a.timestamp.localeCompare(b.timestamp));
|
|
131
|
+
result.push({
|
|
132
|
+
id: conversationId,
|
|
133
|
+
conversationId,
|
|
134
|
+
logs: sorted,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
result.sort((a, b) => (a.logs[0]?.timestamp ?? "").localeCompare(b.logs[0]?.timestamp ?? ""));
|
|
139
|
+
|
|
140
|
+
return result;
|
|
141
|
+
}
|