@lssm/module.ai-chat 0.0.0-canary-20251217060834 → 0.0.0-canary-20251217072406
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ai-chat.feature.js +93 -1
- package/dist/context/context-builder.js +147 -2
- package/dist/context/file-operations.js +174 -1
- package/dist/context/index.js +5 -1
- package/dist/context/workspace-context.js +123 -2
- package/dist/core/chat-service.js +211 -2
- package/dist/core/conversation-store.js +108 -1
- package/dist/core/index.js +4 -1
- package/dist/index.d.ts +0 -2
- package/dist/index.js +22 -1
- package/dist/libs/ai-providers/dist/factory.js +225 -0
- package/dist/libs/ai-providers/dist/index.js +4 -0
- package/dist/libs/ai-providers/dist/legacy.js +2 -0
- package/dist/libs/ai-providers/dist/models.js +299 -0
- package/dist/libs/ai-providers/dist/validation.js +60 -0
- package/dist/libs/design-system/dist/_virtual/rolldown_runtime.js +5 -0
- package/dist/libs/design-system/dist/components/atoms/Button.js +33 -0
- package/dist/libs/design-system/dist/components/atoms/Textarea.js +35 -0
- package/dist/libs/design-system/dist/lib/keyboard.js +193 -0
- package/dist/libs/design-system/dist/ui-kit-web/dist/ui/button.js +55 -0
- package/dist/libs/design-system/dist/ui-kit-web/dist/ui/textarea.js +16 -0
- package/dist/libs/design-system/dist/ui-kit-web/dist/ui-kit-core/dist/utils.js +13 -0
- package/dist/libs/ui-kit-web/dist/ui/avatar.js +25 -0
- package/dist/libs/ui-kit-web/dist/ui/badge.js +26 -0
- package/dist/libs/ui-kit-web/dist/ui/scroll-area.js +39 -0
- package/dist/libs/ui-kit-web/dist/ui/select.js +79 -0
- package/dist/libs/ui-kit-web/dist/ui/skeleton.js +14 -0
- package/dist/libs/ui-kit-web/dist/ui/tooltip.js +39 -0
- package/dist/libs/ui-kit-web/dist/ui/utils.js +10 -0
- package/dist/libs/ui-kit-web/dist/ui-kit-core/dist/utils.js +10 -0
- package/dist/presentation/components/ChatContainer.js +62 -1
- package/dist/presentation/components/ChatInput.js +149 -1
- package/dist/presentation/components/ChatMessage.js +135 -1
- package/dist/presentation/components/CodePreview.js +126 -2
- package/dist/presentation/components/ContextIndicator.js +96 -1
- package/dist/presentation/components/ModelPicker.d.ts +1 -1
- package/dist/presentation/components/ModelPicker.js +197 -1
- package/dist/presentation/components/index.js +8 -1
- package/dist/presentation/hooks/index.js +4 -1
- package/dist/presentation/hooks/useChat.js +171 -1
- package/dist/presentation/hooks/useProviders.js +42 -1
- package/dist/presentation/index.d.ts +0 -1
- package/dist/presentation/index.js +12 -1
- package/dist/providers/chat-utilities.js +16 -1
- package/dist/providers/index.js +7 -1
- package/package.json +10 -10
|
@@ -1,2 +1,126 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { cn } from "../../libs/ui-kit-web/dist/ui/utils.js";
|
|
4
|
+
import { Button$1 } from "../../libs/design-system/dist/components/atoms/Button.js";
|
|
5
|
+
import * as React from "react";
|
|
6
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
7
|
+
import { Check, Copy, Download, Play } from "lucide-react";
|
|
8
|
+
|
|
9
|
+
//#region src/presentation/components/CodePreview.tsx
|
|
10
|
+
/**
|
|
11
|
+
* Language display names
|
|
12
|
+
*/
|
|
13
|
+
const LANGUAGE_NAMES = {
|
|
14
|
+
ts: "TypeScript",
|
|
15
|
+
tsx: "TypeScript (React)",
|
|
16
|
+
typescript: "TypeScript",
|
|
17
|
+
js: "JavaScript",
|
|
18
|
+
jsx: "JavaScript (React)",
|
|
19
|
+
javascript: "JavaScript",
|
|
20
|
+
json: "JSON",
|
|
21
|
+
md: "Markdown",
|
|
22
|
+
yaml: "YAML",
|
|
23
|
+
yml: "YAML",
|
|
24
|
+
bash: "Bash",
|
|
25
|
+
sh: "Shell",
|
|
26
|
+
sql: "SQL",
|
|
27
|
+
py: "Python",
|
|
28
|
+
python: "Python",
|
|
29
|
+
go: "Go",
|
|
30
|
+
rust: "Rust",
|
|
31
|
+
rs: "Rust"
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Code preview component with syntax highlighting placeholder
|
|
35
|
+
*/
|
|
36
|
+
function CodePreview({ code, language = "text", filename, className, showCopy = true, showExecute = false, onExecute, showDownload = false, maxHeight = 400 }) {
|
|
37
|
+
const [copied, setCopied] = React.useState(false);
|
|
38
|
+
const displayLanguage = LANGUAGE_NAMES[language.toLowerCase()] ?? language;
|
|
39
|
+
const lines = code.split("\n");
|
|
40
|
+
const handleCopy = React.useCallback(async () => {
|
|
41
|
+
await navigator.clipboard.writeText(code);
|
|
42
|
+
setCopied(true);
|
|
43
|
+
setTimeout(() => setCopied(false), 2e3);
|
|
44
|
+
}, [code]);
|
|
45
|
+
const handleDownload = React.useCallback(() => {
|
|
46
|
+
const blob = new Blob([code], { type: "text/plain" });
|
|
47
|
+
const url = URL.createObjectURL(blob);
|
|
48
|
+
const a = document.createElement("a");
|
|
49
|
+
a.href = url;
|
|
50
|
+
a.download = filename ?? `code.${language}`;
|
|
51
|
+
document.body.appendChild(a);
|
|
52
|
+
a.click();
|
|
53
|
+
document.body.removeChild(a);
|
|
54
|
+
URL.revokeObjectURL(url);
|
|
55
|
+
}, [
|
|
56
|
+
code,
|
|
57
|
+
filename,
|
|
58
|
+
language
|
|
59
|
+
]);
|
|
60
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
61
|
+
className: cn("overflow-hidden rounded-lg border", "bg-muted/50", className),
|
|
62
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
63
|
+
className: cn("flex items-center justify-between px-3 py-1.5", "border-b bg-muted/80"),
|
|
64
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
65
|
+
className: "flex items-center gap-2 text-sm",
|
|
66
|
+
children: [filename && /* @__PURE__ */ jsx("span", {
|
|
67
|
+
className: "text-foreground font-mono",
|
|
68
|
+
children: filename
|
|
69
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
70
|
+
className: "text-muted-foreground",
|
|
71
|
+
children: displayLanguage
|
|
72
|
+
})]
|
|
73
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
74
|
+
className: "flex items-center gap-1",
|
|
75
|
+
children: [
|
|
76
|
+
showExecute && onExecute && /* @__PURE__ */ jsx(Button$1, {
|
|
77
|
+
variant: "ghost",
|
|
78
|
+
size: "sm",
|
|
79
|
+
onPress: () => onExecute(code),
|
|
80
|
+
className: "h-7 w-7 p-0",
|
|
81
|
+
"aria-label": "Execute code",
|
|
82
|
+
children: /* @__PURE__ */ jsx(Play, { className: "h-3.5 w-3.5" })
|
|
83
|
+
}),
|
|
84
|
+
showDownload && /* @__PURE__ */ jsx(Button$1, {
|
|
85
|
+
variant: "ghost",
|
|
86
|
+
size: "sm",
|
|
87
|
+
onPress: handleDownload,
|
|
88
|
+
className: "h-7 w-7 p-0",
|
|
89
|
+
"aria-label": "Download code",
|
|
90
|
+
children: /* @__PURE__ */ jsx(Download, { className: "h-3.5 w-3.5" })
|
|
91
|
+
}),
|
|
92
|
+
showCopy && /* @__PURE__ */ jsx(Button$1, {
|
|
93
|
+
variant: "ghost",
|
|
94
|
+
size: "sm",
|
|
95
|
+
onPress: handleCopy,
|
|
96
|
+
className: "h-7 w-7 p-0",
|
|
97
|
+
"aria-label": copied ? "Copied" : "Copy code",
|
|
98
|
+
children: copied ? /* @__PURE__ */ jsx(Check, { className: "h-3.5 w-3.5 text-green-500" }) : /* @__PURE__ */ jsx(Copy, { className: "h-3.5 w-3.5" })
|
|
99
|
+
})
|
|
100
|
+
]
|
|
101
|
+
})]
|
|
102
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
103
|
+
className: "overflow-auto",
|
|
104
|
+
style: { maxHeight },
|
|
105
|
+
children: /* @__PURE__ */ jsx("pre", {
|
|
106
|
+
className: "p-3",
|
|
107
|
+
children: /* @__PURE__ */ jsx("code", {
|
|
108
|
+
className: "text-sm",
|
|
109
|
+
children: lines.map((line, i) => /* @__PURE__ */ jsxs("div", {
|
|
110
|
+
className: "flex",
|
|
111
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
112
|
+
className: "text-muted-foreground mr-4 select-none text-right w-8",
|
|
113
|
+
children: i + 1
|
|
114
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
115
|
+
className: "flex-1",
|
|
116
|
+
children: line || " "
|
|
117
|
+
})]
|
|
118
|
+
}, i))
|
|
119
|
+
})
|
|
120
|
+
})
|
|
121
|
+
})]
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
//#endregion
|
|
126
|
+
export { CodePreview };
|
|
@@ -1 +1,96 @@
|
|
|
1
|
-
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { cn } from "../../libs/ui-kit-web/dist/ui/utils.js";
|
|
4
|
+
import { Badge } from "../../libs/ui-kit-web/dist/ui/badge.js";
|
|
5
|
+
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../../libs/ui-kit-web/dist/ui/tooltip.js";
|
|
6
|
+
import "react";
|
|
7
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
8
|
+
import { FileCode, FolderOpen, Info, Zap } from "lucide-react";
|
|
9
|
+
|
|
10
|
+
//#region src/presentation/components/ContextIndicator.tsx
|
|
11
|
+
/**
|
|
12
|
+
* Indicator showing active workspace context
|
|
13
|
+
*/
|
|
14
|
+
function ContextIndicator({ summary, active = false, className, showDetails = true }) {
|
|
15
|
+
if (!summary && !active) return /* @__PURE__ */ jsxs("div", {
|
|
16
|
+
className: cn("flex items-center gap-1.5 text-sm", "text-muted-foreground", className),
|
|
17
|
+
children: [/* @__PURE__ */ jsx(Info, { className: "h-4 w-4" }), /* @__PURE__ */ jsx("span", { children: "No workspace context" })]
|
|
18
|
+
});
|
|
19
|
+
const content = /* @__PURE__ */ jsxs("div", {
|
|
20
|
+
className: cn("flex items-center gap-2", active ? "text-foreground" : "text-muted-foreground", className),
|
|
21
|
+
children: [/* @__PURE__ */ jsxs(Badge, {
|
|
22
|
+
variant: active ? "default" : "secondary",
|
|
23
|
+
className: "flex items-center gap-1",
|
|
24
|
+
children: [/* @__PURE__ */ jsx(Zap, { className: "h-3 w-3" }), "Context"]
|
|
25
|
+
}), summary && showDetails && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("div", {
|
|
26
|
+
className: "flex items-center gap-1 text-xs",
|
|
27
|
+
children: [/* @__PURE__ */ jsx(FolderOpen, { className: "h-3.5 w-3.5" }), /* @__PURE__ */ jsx("span", { children: summary.name })]
|
|
28
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
29
|
+
className: "flex items-center gap-1 text-xs",
|
|
30
|
+
children: [/* @__PURE__ */ jsx(FileCode, { className: "h-3.5 w-3.5" }), /* @__PURE__ */ jsxs("span", { children: [summary.specs.total, " specs"] })]
|
|
31
|
+
})] })]
|
|
32
|
+
});
|
|
33
|
+
if (!summary) return content;
|
|
34
|
+
return /* @__PURE__ */ jsx(TooltipProvider, { children: /* @__PURE__ */ jsxs(Tooltip, { children: [/* @__PURE__ */ jsx(TooltipTrigger, {
|
|
35
|
+
asChild: true,
|
|
36
|
+
children: content
|
|
37
|
+
}), /* @__PURE__ */ jsx(TooltipContent, {
|
|
38
|
+
side: "bottom",
|
|
39
|
+
className: "max-w-[300px]",
|
|
40
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
41
|
+
className: "flex flex-col gap-2 text-sm",
|
|
42
|
+
children: [
|
|
43
|
+
/* @__PURE__ */ jsx("div", {
|
|
44
|
+
className: "font-medium",
|
|
45
|
+
children: summary.name
|
|
46
|
+
}),
|
|
47
|
+
/* @__PURE__ */ jsx("div", {
|
|
48
|
+
className: "text-muted-foreground text-xs",
|
|
49
|
+
children: summary.path
|
|
50
|
+
}),
|
|
51
|
+
/* @__PURE__ */ jsx("div", {
|
|
52
|
+
className: "border-t pt-2",
|
|
53
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
54
|
+
className: "grid grid-cols-2 gap-1 text-xs",
|
|
55
|
+
children: [
|
|
56
|
+
/* @__PURE__ */ jsx("span", { children: "Commands:" }),
|
|
57
|
+
/* @__PURE__ */ jsx("span", {
|
|
58
|
+
className: "text-right",
|
|
59
|
+
children: summary.specs.commands
|
|
60
|
+
}),
|
|
61
|
+
/* @__PURE__ */ jsx("span", { children: "Queries:" }),
|
|
62
|
+
/* @__PURE__ */ jsx("span", {
|
|
63
|
+
className: "text-right",
|
|
64
|
+
children: summary.specs.queries
|
|
65
|
+
}),
|
|
66
|
+
/* @__PURE__ */ jsx("span", { children: "Events:" }),
|
|
67
|
+
/* @__PURE__ */ jsx("span", {
|
|
68
|
+
className: "text-right",
|
|
69
|
+
children: summary.specs.events
|
|
70
|
+
}),
|
|
71
|
+
/* @__PURE__ */ jsx("span", { children: "Presentations:" }),
|
|
72
|
+
/* @__PURE__ */ jsx("span", {
|
|
73
|
+
className: "text-right",
|
|
74
|
+
children: summary.specs.presentations
|
|
75
|
+
})
|
|
76
|
+
]
|
|
77
|
+
})
|
|
78
|
+
}),
|
|
79
|
+
/* @__PURE__ */ jsxs("div", {
|
|
80
|
+
className: "border-t pt-2 text-xs",
|
|
81
|
+
children: [
|
|
82
|
+
/* @__PURE__ */ jsxs("span", { children: [summary.files.total, " files"] }),
|
|
83
|
+
/* @__PURE__ */ jsx("span", {
|
|
84
|
+
className: "mx-1",
|
|
85
|
+
children: "•"
|
|
86
|
+
}),
|
|
87
|
+
/* @__PURE__ */ jsxs("span", { children: [summary.files.specFiles, " spec files"] })
|
|
88
|
+
]
|
|
89
|
+
})
|
|
90
|
+
]
|
|
91
|
+
})
|
|
92
|
+
})] }) });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
//#endregion
|
|
96
|
+
export { ContextIndicator };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { ProviderMode, ProviderName } from "@lssm/lib.ai-providers";
|
|
2
1
|
import * as react_jsx_runtime3 from "react/jsx-runtime";
|
|
2
|
+
import { ProviderMode, ProviderName } from "@lssm/lib.ai-providers";
|
|
3
3
|
|
|
4
4
|
//#region src/presentation/components/ModelPicker.d.ts
|
|
5
5
|
interface ModelSelection {
|
|
@@ -1 +1,197 @@
|
|
|
1
|
-
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { getModelsForProvider } from "../../libs/ai-providers/dist/models.js";
|
|
4
|
+
import "../../libs/ai-providers/dist/index.js";
|
|
5
|
+
import { cn } from "../../libs/ui-kit-web/dist/ui/utils.js";
|
|
6
|
+
import { Button$1 } from "../../libs/design-system/dist/components/atoms/Button.js";
|
|
7
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../../libs/ui-kit-web/dist/ui/select.js";
|
|
8
|
+
import { Badge } from "../../libs/ui-kit-web/dist/ui/badge.js";
|
|
9
|
+
import * as React from "react";
|
|
10
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
11
|
+
import { Bot, Cloud, Cpu, Sparkles } from "lucide-react";
|
|
12
|
+
|
|
13
|
+
//#region src/presentation/components/ModelPicker.tsx
|
|
14
|
+
const PROVIDER_ICONS = {
|
|
15
|
+
ollama: /* @__PURE__ */ jsx(Cpu, { className: "h-4 w-4" }),
|
|
16
|
+
openai: /* @__PURE__ */ jsx(Bot, { className: "h-4 w-4" }),
|
|
17
|
+
anthropic: /* @__PURE__ */ jsx(Sparkles, { className: "h-4 w-4" }),
|
|
18
|
+
mistral: /* @__PURE__ */ jsx(Cloud, { className: "h-4 w-4" }),
|
|
19
|
+
gemini: /* @__PURE__ */ jsx(Sparkles, { className: "h-4 w-4" })
|
|
20
|
+
};
|
|
21
|
+
const PROVIDER_NAMES = {
|
|
22
|
+
ollama: "Ollama (Local)",
|
|
23
|
+
openai: "OpenAI",
|
|
24
|
+
anthropic: "Anthropic",
|
|
25
|
+
mistral: "Mistral",
|
|
26
|
+
gemini: "Google Gemini"
|
|
27
|
+
};
|
|
28
|
+
const MODE_BADGES = {
|
|
29
|
+
local: {
|
|
30
|
+
label: "Local",
|
|
31
|
+
variant: "secondary"
|
|
32
|
+
},
|
|
33
|
+
byok: {
|
|
34
|
+
label: "BYOK",
|
|
35
|
+
variant: "outline"
|
|
36
|
+
},
|
|
37
|
+
managed: {
|
|
38
|
+
label: "Managed",
|
|
39
|
+
variant: "default"
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Model picker component for selecting AI provider and model
|
|
44
|
+
*/
|
|
45
|
+
function ModelPicker({ value, onChange, availableProviders, className, compact = false }) {
|
|
46
|
+
const providers = availableProviders ?? [
|
|
47
|
+
{
|
|
48
|
+
provider: "ollama",
|
|
49
|
+
available: true,
|
|
50
|
+
mode: "local"
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
provider: "openai",
|
|
54
|
+
available: true,
|
|
55
|
+
mode: "byok"
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
provider: "anthropic",
|
|
59
|
+
available: true,
|
|
60
|
+
mode: "byok"
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
provider: "mistral",
|
|
64
|
+
available: true,
|
|
65
|
+
mode: "byok"
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
provider: "gemini",
|
|
69
|
+
available: true,
|
|
70
|
+
mode: "byok"
|
|
71
|
+
}
|
|
72
|
+
];
|
|
73
|
+
const models = getModelsForProvider(value.provider);
|
|
74
|
+
const selectedModel = models.find((m) => m.id === value.model);
|
|
75
|
+
const handleProviderChange = React.useCallback((providerName) => {
|
|
76
|
+
const provider = providerName;
|
|
77
|
+
const providerInfo = providers.find((p) => p.provider === provider);
|
|
78
|
+
onChange({
|
|
79
|
+
provider,
|
|
80
|
+
model: getModelsForProvider(provider)[0]?.id ?? "",
|
|
81
|
+
mode: providerInfo?.mode ?? "byok"
|
|
82
|
+
});
|
|
83
|
+
}, [onChange, providers]);
|
|
84
|
+
const handleModelChange = React.useCallback((modelId) => {
|
|
85
|
+
onChange({
|
|
86
|
+
...value,
|
|
87
|
+
model: modelId
|
|
88
|
+
});
|
|
89
|
+
}, [onChange, value]);
|
|
90
|
+
if (compact) return /* @__PURE__ */ jsxs("div", {
|
|
91
|
+
className: cn("flex items-center gap-2", className),
|
|
92
|
+
children: [/* @__PURE__ */ jsxs(Select, {
|
|
93
|
+
value: value.provider,
|
|
94
|
+
onValueChange: handleProviderChange,
|
|
95
|
+
children: [/* @__PURE__ */ jsx(SelectTrigger, {
|
|
96
|
+
className: "w-[140px]",
|
|
97
|
+
children: /* @__PURE__ */ jsx(SelectValue, {})
|
|
98
|
+
}), /* @__PURE__ */ jsx(SelectContent, { children: providers.map((p) => /* @__PURE__ */ jsx(SelectItem, {
|
|
99
|
+
value: p.provider,
|
|
100
|
+
disabled: !p.available,
|
|
101
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
102
|
+
className: "flex items-center gap-2",
|
|
103
|
+
children: [PROVIDER_ICONS[p.provider], /* @__PURE__ */ jsx("span", { children: PROVIDER_NAMES[p.provider] })]
|
|
104
|
+
})
|
|
105
|
+
}, p.provider)) })]
|
|
106
|
+
}), /* @__PURE__ */ jsxs(Select, {
|
|
107
|
+
value: value.model,
|
|
108
|
+
onValueChange: handleModelChange,
|
|
109
|
+
children: [/* @__PURE__ */ jsx(SelectTrigger, {
|
|
110
|
+
className: "w-[160px]",
|
|
111
|
+
children: /* @__PURE__ */ jsx(SelectValue, {})
|
|
112
|
+
}), /* @__PURE__ */ jsx(SelectContent, { children: models.map((m) => /* @__PURE__ */ jsx(SelectItem, {
|
|
113
|
+
value: m.id,
|
|
114
|
+
children: m.name
|
|
115
|
+
}, m.id)) })]
|
|
116
|
+
})]
|
|
117
|
+
});
|
|
118
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
119
|
+
className: cn("flex flex-col gap-3", className),
|
|
120
|
+
children: [
|
|
121
|
+
/* @__PURE__ */ jsxs("div", {
|
|
122
|
+
className: "flex flex-col gap-1.5",
|
|
123
|
+
children: [/* @__PURE__ */ jsx("label", {
|
|
124
|
+
className: "text-sm font-medium",
|
|
125
|
+
children: "Provider"
|
|
126
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
127
|
+
className: "flex flex-wrap gap-2",
|
|
128
|
+
children: providers.map((p) => /* @__PURE__ */ jsxs(Button$1, {
|
|
129
|
+
variant: value.provider === p.provider ? "default" : "outline",
|
|
130
|
+
size: "sm",
|
|
131
|
+
onPress: () => p.available && handleProviderChange(p.provider),
|
|
132
|
+
disabled: !p.available,
|
|
133
|
+
className: cn(!p.available && "opacity-50"),
|
|
134
|
+
children: [
|
|
135
|
+
PROVIDER_ICONS[p.provider],
|
|
136
|
+
/* @__PURE__ */ jsx("span", { children: PROVIDER_NAMES[p.provider] }),
|
|
137
|
+
/* @__PURE__ */ jsx(Badge, {
|
|
138
|
+
variant: MODE_BADGES[p.mode].variant,
|
|
139
|
+
className: "ml-1",
|
|
140
|
+
children: MODE_BADGES[p.mode].label
|
|
141
|
+
})
|
|
142
|
+
]
|
|
143
|
+
}, p.provider))
|
|
144
|
+
})]
|
|
145
|
+
}),
|
|
146
|
+
/* @__PURE__ */ jsxs("div", {
|
|
147
|
+
className: "flex flex-col gap-1.5",
|
|
148
|
+
children: [/* @__PURE__ */ jsx("label", {
|
|
149
|
+
className: "text-sm font-medium",
|
|
150
|
+
children: "Model"
|
|
151
|
+
}), /* @__PURE__ */ jsxs(Select, {
|
|
152
|
+
value: value.model,
|
|
153
|
+
onValueChange: handleModelChange,
|
|
154
|
+
children: [/* @__PURE__ */ jsx(SelectTrigger, { children: /* @__PURE__ */ jsx(SelectValue, { placeholder: "Select a model" }) }), /* @__PURE__ */ jsx(SelectContent, { children: models.map((m) => /* @__PURE__ */ jsx(SelectItem, {
|
|
155
|
+
value: m.id,
|
|
156
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
157
|
+
className: "flex items-center gap-2",
|
|
158
|
+
children: [
|
|
159
|
+
/* @__PURE__ */ jsx("span", { children: m.name }),
|
|
160
|
+
/* @__PURE__ */ jsxs("span", {
|
|
161
|
+
className: "text-muted-foreground text-xs",
|
|
162
|
+
children: [Math.round(m.contextWindow / 1e3), "K"]
|
|
163
|
+
}),
|
|
164
|
+
m.capabilities.vision && /* @__PURE__ */ jsx(Badge, {
|
|
165
|
+
variant: "outline",
|
|
166
|
+
className: "text-xs",
|
|
167
|
+
children: "Vision"
|
|
168
|
+
}),
|
|
169
|
+
m.capabilities.reasoning && /* @__PURE__ */ jsx(Badge, {
|
|
170
|
+
variant: "outline",
|
|
171
|
+
className: "text-xs",
|
|
172
|
+
children: "Reasoning"
|
|
173
|
+
})
|
|
174
|
+
]
|
|
175
|
+
})
|
|
176
|
+
}, m.id)) })]
|
|
177
|
+
})]
|
|
178
|
+
}),
|
|
179
|
+
selectedModel && /* @__PURE__ */ jsxs("div", {
|
|
180
|
+
className: "text-muted-foreground flex flex-wrap gap-2 text-xs",
|
|
181
|
+
children: [
|
|
182
|
+
/* @__PURE__ */ jsxs("span", { children: [
|
|
183
|
+
"Context: ",
|
|
184
|
+
Math.round(selectedModel.contextWindow / 1e3),
|
|
185
|
+
"K tokens"
|
|
186
|
+
] }),
|
|
187
|
+
selectedModel.capabilities.vision && /* @__PURE__ */ jsx("span", { children: "• Vision" }),
|
|
188
|
+
selectedModel.capabilities.tools && /* @__PURE__ */ jsx("span", { children: "• Tools" }),
|
|
189
|
+
selectedModel.capabilities.reasoning && /* @__PURE__ */ jsx("span", { children: "• Reasoning" })
|
|
190
|
+
]
|
|
191
|
+
})
|
|
192
|
+
]
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
//#endregion
|
|
197
|
+
export { ModelPicker };
|
|
@@ -1 +1,8 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import { ChatContainer } from "./ChatContainer.js";
|
|
2
|
+
import { CodePreview } from "./CodePreview.js";
|
|
3
|
+
import { ChatMessage } from "./ChatMessage.js";
|
|
4
|
+
import { ChatInput } from "./ChatInput.js";
|
|
5
|
+
import { ModelPicker } from "./ModelPicker.js";
|
|
6
|
+
import { ContextIndicator } from "./ContextIndicator.js";
|
|
7
|
+
|
|
8
|
+
export { ChatContainer, ChatInput, ChatMessage, CodePreview, ContextIndicator, ModelPicker };
|
|
@@ -1 +1,171 @@
|
|
|
1
|
-
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { ChatService } from "../../core/chat-service.js";
|
|
4
|
+
import { createProvider } from "../../libs/ai-providers/dist/factory.js";
|
|
5
|
+
import "../../libs/ai-providers/dist/index.js";
|
|
6
|
+
import * as React from "react";
|
|
7
|
+
|
|
8
|
+
//#region src/presentation/hooks/useChat.tsx
|
|
9
|
+
/**
|
|
10
|
+
* Hook for managing AI chat state
|
|
11
|
+
*/
|
|
12
|
+
function useChat(options = {}) {
|
|
13
|
+
const { provider = "openai", mode = "byok", model, apiKey, proxyUrl, conversationId: initialConversationId, systemPrompt, streaming = true, onSend, onResponse, onError, onUsage } = options;
|
|
14
|
+
const [messages, setMessages] = React.useState([]);
|
|
15
|
+
const [conversation, setConversation] = React.useState(null);
|
|
16
|
+
const [isLoading, setIsLoading] = React.useState(false);
|
|
17
|
+
const [error, setError] = React.useState(null);
|
|
18
|
+
const [conversationId, setConversationId] = React.useState(initialConversationId ?? null);
|
|
19
|
+
const abortControllerRef = React.useRef(null);
|
|
20
|
+
const chatServiceRef = React.useRef(null);
|
|
21
|
+
React.useEffect(() => {
|
|
22
|
+
chatServiceRef.current = new ChatService({
|
|
23
|
+
provider: createProvider({
|
|
24
|
+
provider,
|
|
25
|
+
model,
|
|
26
|
+
apiKey,
|
|
27
|
+
proxyUrl
|
|
28
|
+
}),
|
|
29
|
+
systemPrompt,
|
|
30
|
+
onUsage
|
|
31
|
+
});
|
|
32
|
+
}, [
|
|
33
|
+
provider,
|
|
34
|
+
mode,
|
|
35
|
+
model,
|
|
36
|
+
apiKey,
|
|
37
|
+
proxyUrl,
|
|
38
|
+
systemPrompt,
|
|
39
|
+
onUsage
|
|
40
|
+
]);
|
|
41
|
+
React.useEffect(() => {
|
|
42
|
+
if (!conversationId || !chatServiceRef.current) return;
|
|
43
|
+
const loadConversation = async () => {
|
|
44
|
+
const conv = await chatServiceRef.current.getConversation(conversationId);
|
|
45
|
+
if (conv) {
|
|
46
|
+
setConversation(conv);
|
|
47
|
+
setMessages(conv.messages);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
loadConversation().catch(console.error);
|
|
51
|
+
}, [conversationId]);
|
|
52
|
+
const sendMessage = React.useCallback(async (content, attachments) => {
|
|
53
|
+
if (!chatServiceRef.current) throw new Error("Chat service not initialized");
|
|
54
|
+
setIsLoading(true);
|
|
55
|
+
setError(null);
|
|
56
|
+
abortControllerRef.current = new AbortController();
|
|
57
|
+
try {
|
|
58
|
+
const userMessage = {
|
|
59
|
+
id: `msg_${Date.now()}`,
|
|
60
|
+
conversationId: conversationId ?? "",
|
|
61
|
+
role: "user",
|
|
62
|
+
content,
|
|
63
|
+
status: "completed",
|
|
64
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
65
|
+
updatedAt: /* @__PURE__ */ new Date(),
|
|
66
|
+
attachments
|
|
67
|
+
};
|
|
68
|
+
setMessages((prev) => [...prev, userMessage]);
|
|
69
|
+
onSend?.(userMessage);
|
|
70
|
+
if (streaming) {
|
|
71
|
+
const result = await chatServiceRef.current.stream({
|
|
72
|
+
conversationId: conversationId ?? void 0,
|
|
73
|
+
content,
|
|
74
|
+
attachments
|
|
75
|
+
});
|
|
76
|
+
if (!conversationId) setConversationId(result.conversationId);
|
|
77
|
+
const assistantMessage = {
|
|
78
|
+
id: result.messageId,
|
|
79
|
+
conversationId: result.conversationId,
|
|
80
|
+
role: "assistant",
|
|
81
|
+
content: "",
|
|
82
|
+
status: "streaming",
|
|
83
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
84
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
85
|
+
};
|
|
86
|
+
setMessages((prev) => [...prev, assistantMessage]);
|
|
87
|
+
let fullContent = "";
|
|
88
|
+
for await (const chunk of result.stream) if (chunk.type === "text" && chunk.content) {
|
|
89
|
+
fullContent += chunk.content;
|
|
90
|
+
setMessages((prev) => prev.map((m) => m.id === result.messageId ? {
|
|
91
|
+
...m,
|
|
92
|
+
content: fullContent
|
|
93
|
+
} : m));
|
|
94
|
+
} else if (chunk.type === "done") {
|
|
95
|
+
setMessages((prev) => prev.map((m) => m.id === result.messageId ? {
|
|
96
|
+
...m,
|
|
97
|
+
status: "completed",
|
|
98
|
+
usage: chunk.usage,
|
|
99
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
100
|
+
} : m));
|
|
101
|
+
onResponse?.(messages.find((m) => m.id === result.messageId) ?? assistantMessage);
|
|
102
|
+
} else if (chunk.type === "error") {
|
|
103
|
+
setMessages((prev) => prev.map((m) => m.id === result.messageId ? {
|
|
104
|
+
...m,
|
|
105
|
+
status: "error",
|
|
106
|
+
error: chunk.error,
|
|
107
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
108
|
+
} : m));
|
|
109
|
+
if (chunk.error) {
|
|
110
|
+
const err = new Error(chunk.error.message);
|
|
111
|
+
setError(err);
|
|
112
|
+
onError?.(err);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
} else {
|
|
116
|
+
const result = await chatServiceRef.current.send({
|
|
117
|
+
conversationId: conversationId ?? void 0,
|
|
118
|
+
content,
|
|
119
|
+
attachments
|
|
120
|
+
});
|
|
121
|
+
setConversation(result.conversation);
|
|
122
|
+
setMessages(result.conversation.messages);
|
|
123
|
+
if (!conversationId) setConversationId(result.conversation.id);
|
|
124
|
+
onResponse?.(result.message);
|
|
125
|
+
}
|
|
126
|
+
} catch (err) {
|
|
127
|
+
const error$1 = err instanceof Error ? err : new Error(String(err));
|
|
128
|
+
setError(error$1);
|
|
129
|
+
onError?.(error$1);
|
|
130
|
+
} finally {
|
|
131
|
+
setIsLoading(false);
|
|
132
|
+
abortControllerRef.current = null;
|
|
133
|
+
}
|
|
134
|
+
}, [
|
|
135
|
+
conversationId,
|
|
136
|
+
streaming,
|
|
137
|
+
onSend,
|
|
138
|
+
onResponse,
|
|
139
|
+
onError,
|
|
140
|
+
messages
|
|
141
|
+
]);
|
|
142
|
+
return {
|
|
143
|
+
messages,
|
|
144
|
+
conversation,
|
|
145
|
+
isLoading,
|
|
146
|
+
error,
|
|
147
|
+
sendMessage,
|
|
148
|
+
clearConversation: React.useCallback(() => {
|
|
149
|
+
setMessages([]);
|
|
150
|
+
setConversation(null);
|
|
151
|
+
setConversationId(null);
|
|
152
|
+
setError(null);
|
|
153
|
+
}, []),
|
|
154
|
+
setConversationId,
|
|
155
|
+
regenerate: React.useCallback(async () => {
|
|
156
|
+
const lastUserMessageIndex = messages.findLastIndex((m) => m.role === "user");
|
|
157
|
+
if (lastUserMessageIndex === -1) return;
|
|
158
|
+
const lastUserMessage = messages[lastUserMessageIndex];
|
|
159
|
+
if (!lastUserMessage) return;
|
|
160
|
+
setMessages((prev) => prev.slice(0, lastUserMessageIndex + 1));
|
|
161
|
+
await sendMessage(lastUserMessage.content, lastUserMessage.attachments);
|
|
162
|
+
}, [messages, sendMessage]),
|
|
163
|
+
stop: React.useCallback(() => {
|
|
164
|
+
abortControllerRef.current?.abort();
|
|
165
|
+
setIsLoading(false);
|
|
166
|
+
}, [])
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
//#endregion
|
|
171
|
+
export { useChat };
|