@contractspec/module.ai-chat 0.0.0-canary-20260113170453
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/LICENSE +21 -0
- package/README.md +169 -0
- package/dist/ai-chat.feature.d.ts +12 -0
- package/dist/ai-chat.feature.d.ts.map +1 -0
- package/dist/ai-chat.feature.js +102 -0
- package/dist/ai-chat.feature.js.map +1 -0
- package/dist/ai-chat.operations.d.ts +243 -0
- package/dist/ai-chat.operations.d.ts.map +1 -0
- package/dist/ai-chat.operations.js +172 -0
- package/dist/ai-chat.operations.js.map +1 -0
- package/dist/context/context-builder.d.ts +57 -0
- package/dist/context/context-builder.d.ts.map +1 -0
- package/dist/context/context-builder.js +148 -0
- package/dist/context/context-builder.js.map +1 -0
- package/dist/context/file-operations.d.ts +100 -0
- package/dist/context/file-operations.d.ts.map +1 -0
- package/dist/context/file-operations.js +175 -0
- package/dist/context/file-operations.js.map +1 -0
- package/dist/context/index.d.ts +4 -0
- package/dist/context/index.js +5 -0
- package/dist/context/workspace-context.d.ts +117 -0
- package/dist/context/workspace-context.d.ts.map +1 -0
- package/dist/context/workspace-context.js +124 -0
- package/dist/context/workspace-context.js.map +1 -0
- package/dist/core/chat-service.d.ts +73 -0
- package/dist/core/chat-service.d.ts.map +1 -0
- package/dist/core/chat-service.js +227 -0
- package/dist/core/chat-service.js.map +1 -0
- package/dist/core/conversation-store.d.ts +74 -0
- package/dist/core/conversation-store.d.ts.map +1 -0
- package/dist/core/conversation-store.js +109 -0
- package/dist/core/conversation-store.js.map +1 -0
- package/dist/core/index.d.ts +4 -0
- package/dist/core/index.js +4 -0
- package/dist/core/message-types.d.ts +150 -0
- package/dist/core/message-types.d.ts.map +1 -0
- package/dist/events.d.ts +115 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +98 -0
- package/dist/events.js.map +1 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +23 -0
- package/dist/presentation/components/ChatContainer.d.ts +21 -0
- package/dist/presentation/components/ChatContainer.d.ts.map +1 -0
- package/dist/presentation/components/ChatContainer.js +63 -0
- package/dist/presentation/components/ChatContainer.js.map +1 -0
- package/dist/presentation/components/ChatInput.d.ts +35 -0
- package/dist/presentation/components/ChatInput.d.ts.map +1 -0
- package/dist/presentation/components/ChatInput.js +149 -0
- package/dist/presentation/components/ChatInput.js.map +1 -0
- package/dist/presentation/components/ChatMessage.d.ts +24 -0
- package/dist/presentation/components/ChatMessage.d.ts.map +1 -0
- package/dist/presentation/components/ChatMessage.js +136 -0
- package/dist/presentation/components/ChatMessage.js.map +1 -0
- package/dist/presentation/components/CodePreview.d.ts +40 -0
- package/dist/presentation/components/CodePreview.d.ts.map +1 -0
- package/dist/presentation/components/CodePreview.js +127 -0
- package/dist/presentation/components/CodePreview.js.map +1 -0
- package/dist/presentation/components/ContextIndicator.d.ts +26 -0
- package/dist/presentation/components/ContextIndicator.d.ts.map +1 -0
- package/dist/presentation/components/ContextIndicator.js +97 -0
- package/dist/presentation/components/ContextIndicator.js.map +1 -0
- package/dist/presentation/components/ModelPicker.d.ts +39 -0
- package/dist/presentation/components/ModelPicker.d.ts.map +1 -0
- package/dist/presentation/components/ModelPicker.js +202 -0
- package/dist/presentation/components/ModelPicker.js.map +1 -0
- package/dist/presentation/components/index.d.ts +7 -0
- package/dist/presentation/components/index.js +8 -0
- package/dist/presentation/hooks/index.d.ts +3 -0
- package/dist/presentation/hooks/index.js +4 -0
- package/dist/presentation/hooks/useChat.d.ts +67 -0
- package/dist/presentation/hooks/useChat.d.ts.map +1 -0
- package/dist/presentation/hooks/useChat.js +172 -0
- package/dist/presentation/hooks/useChat.js.map +1 -0
- package/dist/presentation/hooks/useProviders.d.ts +38 -0
- package/dist/presentation/hooks/useProviders.d.ts.map +1 -0
- package/dist/presentation/hooks/useProviders.js +41 -0
- package/dist/presentation/hooks/useProviders.js.map +1 -0
- package/dist/presentation/index.d.ts +11 -0
- package/dist/presentation/index.js +12 -0
- package/dist/providers/chat-utilities.d.ts +15 -0
- package/dist/providers/chat-utilities.d.ts.map +1 -0
- package/dist/providers/chat-utilities.js +17 -0
- package/dist/providers/chat-utilities.js.map +1 -0
- package/dist/providers/index.d.ts +3 -0
- package/dist/providers/index.js +4 -0
- package/dist/schema.d.ts +222 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +100 -0
- package/dist/schema.js.map +1 -0
- package/package.json +87 -0
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { cn } from "@contractspec/lib.ui-kit-web/ui/utils";
|
|
5
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
6
|
+
import { Bot, Cloud, Cpu, Sparkles } from "lucide-react";
|
|
7
|
+
import { Button } from "@contractspec/lib.design-system";
|
|
8
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@contractspec/lib.ui-kit-web/ui/select";
|
|
9
|
+
import { Badge } from "@contractspec/lib.ui-kit-web/ui/badge";
|
|
10
|
+
import { Label } from "@contractspec/lib.ui-kit-web/ui/label";
|
|
11
|
+
import { getModelsForProvider } from "@contractspec/lib.ai-providers";
|
|
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
|
+
htmlFor: "provider-selection",
|
|
125
|
+
className: "text-sm font-medium",
|
|
126
|
+
children: "Provider"
|
|
127
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
128
|
+
className: "flex flex-wrap gap-2",
|
|
129
|
+
id: "provider-selection",
|
|
130
|
+
children: providers.map((p) => /* @__PURE__ */ jsxs(Button, {
|
|
131
|
+
variant: value.provider === p.provider ? "default" : "outline",
|
|
132
|
+
size: "sm",
|
|
133
|
+
onPress: () => p.available && handleProviderChange(p.provider),
|
|
134
|
+
disabled: !p.available,
|
|
135
|
+
className: cn(!p.available && "opacity-50"),
|
|
136
|
+
children: [
|
|
137
|
+
PROVIDER_ICONS[p.provider],
|
|
138
|
+
/* @__PURE__ */ jsx("span", { children: PROVIDER_NAMES[p.provider] }),
|
|
139
|
+
/* @__PURE__ */ jsx(Badge, {
|
|
140
|
+
variant: MODE_BADGES[p.mode].variant,
|
|
141
|
+
className: "ml-1",
|
|
142
|
+
children: MODE_BADGES[p.mode].label
|
|
143
|
+
})
|
|
144
|
+
]
|
|
145
|
+
}, p.provider))
|
|
146
|
+
})]
|
|
147
|
+
}),
|
|
148
|
+
/* @__PURE__ */ jsxs("div", {
|
|
149
|
+
className: "flex flex-col gap-1.5",
|
|
150
|
+
children: [/* @__PURE__ */ jsx(Label, {
|
|
151
|
+
htmlFor: "model-picker",
|
|
152
|
+
className: "text-sm font-medium",
|
|
153
|
+
children: "Model"
|
|
154
|
+
}), /* @__PURE__ */ jsxs(Select, {
|
|
155
|
+
name: "model-picker",
|
|
156
|
+
value: value.model,
|
|
157
|
+
onValueChange: handleModelChange,
|
|
158
|
+
children: [/* @__PURE__ */ jsx(SelectTrigger, { children: /* @__PURE__ */ jsx(SelectValue, { placeholder: "Select a model" }) }), /* @__PURE__ */ jsx(SelectContent, { children: models.map((m) => /* @__PURE__ */ jsx(SelectItem, {
|
|
159
|
+
value: m.id,
|
|
160
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
161
|
+
className: "flex items-center gap-2",
|
|
162
|
+
children: [
|
|
163
|
+
/* @__PURE__ */ jsx("span", { children: m.name }),
|
|
164
|
+
/* @__PURE__ */ jsxs("span", {
|
|
165
|
+
className: "text-muted-foreground text-xs",
|
|
166
|
+
children: [Math.round(m.contextWindow / 1e3), "K"]
|
|
167
|
+
}),
|
|
168
|
+
m.capabilities.vision && /* @__PURE__ */ jsx(Badge, {
|
|
169
|
+
variant: "outline",
|
|
170
|
+
className: "text-xs",
|
|
171
|
+
children: "Vision"
|
|
172
|
+
}),
|
|
173
|
+
m.capabilities.reasoning && /* @__PURE__ */ jsx(Badge, {
|
|
174
|
+
variant: "outline",
|
|
175
|
+
className: "text-xs",
|
|
176
|
+
children: "Reasoning"
|
|
177
|
+
})
|
|
178
|
+
]
|
|
179
|
+
})
|
|
180
|
+
}, m.id)) })]
|
|
181
|
+
})]
|
|
182
|
+
}),
|
|
183
|
+
selectedModel && /* @__PURE__ */ jsxs("div", {
|
|
184
|
+
className: "text-muted-foreground flex flex-wrap gap-2 text-xs",
|
|
185
|
+
children: [
|
|
186
|
+
/* @__PURE__ */ jsxs("span", { children: [
|
|
187
|
+
"Context: ",
|
|
188
|
+
Math.round(selectedModel.contextWindow / 1e3),
|
|
189
|
+
"K tokens"
|
|
190
|
+
] }),
|
|
191
|
+
selectedModel.capabilities.vision && /* @__PURE__ */ jsx("span", { children: "• Vision" }),
|
|
192
|
+
selectedModel.capabilities.tools && /* @__PURE__ */ jsx("span", { children: "• Tools" }),
|
|
193
|
+
selectedModel.capabilities.reasoning && /* @__PURE__ */ jsx("span", { children: "• Reasoning" })
|
|
194
|
+
]
|
|
195
|
+
})
|
|
196
|
+
]
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
//#endregion
|
|
201
|
+
export { ModelPicker };
|
|
202
|
+
//# sourceMappingURL=ModelPicker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ModelPicker.js","names":[],"sources":["../../../src/presentation/components/ModelPicker.tsx"],"sourcesContent":["'use client';\n\nimport * as React from 'react';\nimport { cn } from '@contractspec/lib.ui-kit-web/ui/utils';\nimport { Button } from '@contractspec/lib.design-system';\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '@contractspec/lib.ui-kit-web/ui/select';\nimport { Badge } from '@contractspec/lib.ui-kit-web/ui/badge';\nimport { Label } from '@contractspec/lib.ui-kit-web/ui/label';\nimport { Bot, Cloud, Cpu, Sparkles } from 'lucide-react';\nimport {\n getModelsForProvider,\n type ModelInfo,\n type ProviderMode,\n type ProviderName,\n} from '@contractspec/lib.ai-providers';\n\nexport interface ModelSelection {\n provider: ProviderName;\n model: string;\n mode: ProviderMode;\n}\n\nexport interface ModelPickerProps {\n /** Currently selected provider/model */\n value: ModelSelection;\n /** Called when selection changes */\n onChange: (value: ModelSelection) => void;\n /** Available providers (with availability info) */\n availableProviders?: {\n provider: ProviderName;\n available: boolean;\n mode: ProviderMode;\n reason?: string;\n }[];\n /** Additional class name */\n className?: string;\n /** Compact mode (smaller) */\n compact?: boolean;\n}\n\nconst PROVIDER_ICONS: Record<ProviderName, React.ReactNode> = {\n ollama: <Cpu className=\"h-4 w-4\" />,\n openai: <Bot className=\"h-4 w-4\" />,\n anthropic: <Sparkles className=\"h-4 w-4\" />,\n mistral: <Cloud className=\"h-4 w-4\" />,\n gemini: <Sparkles className=\"h-4 w-4\" />,\n};\n\nconst PROVIDER_NAMES: Record<ProviderName, string> = {\n ollama: 'Ollama (Local)',\n openai: 'OpenAI',\n anthropic: 'Anthropic',\n mistral: 'Mistral',\n gemini: 'Google Gemini',\n};\n\nconst MODE_BADGES: Record<\n ProviderMode,\n { label: string; variant: 'default' | 'secondary' | 'outline' }\n> = {\n local: { label: 'Local', variant: 'secondary' },\n byok: { label: 'BYOK', variant: 'outline' },\n managed: { label: 'Managed', variant: 'default' },\n};\n\n/**\n * Model picker component for selecting AI provider and model\n */\nexport function ModelPicker({\n value,\n onChange,\n availableProviders,\n className,\n compact = false,\n}: ModelPickerProps) {\n const providers = availableProviders ?? [\n { provider: 'ollama' as const, available: true, mode: 'local' as const },\n { provider: 'openai' as const, available: true, mode: 'byok' as const },\n { provider: 'anthropic' as const, available: true, mode: 'byok' as const },\n { provider: 'mistral' as const, available: true, mode: 'byok' as const },\n { provider: 'gemini' as const, available: true, mode: 'byok' as const },\n ];\n\n const models: ModelInfo[] = getModelsForProvider(value.provider);\n const selectedModel = models.find((m) => m.id === value.model);\n\n const handleProviderChange = React.useCallback(\n (providerName: string) => {\n const provider = providerName as ProviderName;\n const providerInfo = providers.find((p) => p.provider === provider);\n const providerModels = getModelsForProvider(provider);\n const defaultModel = providerModels[0]?.id ?? '';\n\n onChange({\n provider,\n model: defaultModel,\n mode: providerInfo?.mode ?? 'byok',\n });\n },\n [onChange, providers]\n );\n\n const handleModelChange = React.useCallback(\n (modelId: string) => {\n onChange({\n ...value,\n model: modelId,\n });\n },\n [onChange, value]\n );\n\n if (compact) {\n return (\n <div className={cn('flex items-center gap-2', className)}>\n <Select value={value.provider} onValueChange={handleProviderChange}>\n <SelectTrigger className=\"w-[140px]\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {providers.map((p) => (\n <SelectItem\n key={p.provider}\n value={p.provider}\n disabled={!p.available}\n >\n <div className=\"flex items-center gap-2\">\n {PROVIDER_ICONS[p.provider]}\n <span>{PROVIDER_NAMES[p.provider]}</span>\n </div>\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n\n <Select value={value.model} onValueChange={handleModelChange}>\n <SelectTrigger className=\"w-[160px]\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {models.map((m) => (\n <SelectItem key={m.id} value={m.id}>\n {m.name}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n );\n }\n\n return (\n <div className={cn('flex flex-col gap-3', className)}>\n {/* Provider selection */}\n <div className=\"flex flex-col gap-1.5\">\n <Label htmlFor=\"provider-selection\" className=\"text-sm font-medium\">\n Provider\n </Label>\n <div className=\"flex flex-wrap gap-2\" id=\"provider-selection\">\n {providers.map((p) => (\n <Button\n key={p.provider}\n variant={value.provider === p.provider ? 'default' : 'outline'}\n size=\"sm\"\n onPress={() => p.available && handleProviderChange(p.provider)}\n disabled={!p.available}\n className={cn(!p.available && 'opacity-50')}\n >\n {PROVIDER_ICONS[p.provider]}\n <span>{PROVIDER_NAMES[p.provider]}</span>\n <Badge variant={MODE_BADGES[p.mode].variant} className=\"ml-1\">\n {MODE_BADGES[p.mode].label}\n </Badge>\n </Button>\n ))}\n </div>\n </div>\n\n {/* Model selection */}\n <div className=\"flex flex-col gap-1.5\">\n <Label htmlFor=\"model-picker\" className=\"text-sm font-medium\">\n Model\n </Label>\n <Select\n name=\"model-picker\"\n value={value.model}\n onValueChange={handleModelChange}\n >\n <SelectTrigger>\n <SelectValue placeholder=\"Select a model\" />\n </SelectTrigger>\n <SelectContent>\n {models.map((m) => (\n <SelectItem key={m.id} value={m.id}>\n <div className=\"flex items-center gap-2\">\n <span>{m.name}</span>\n <span className=\"text-muted-foreground text-xs\">\n {Math.round(m.contextWindow / 1000)}K\n </span>\n {m.capabilities.vision && (\n <Badge variant=\"outline\" className=\"text-xs\">\n Vision\n </Badge>\n )}\n {m.capabilities.reasoning && (\n <Badge variant=\"outline\" className=\"text-xs\">\n Reasoning\n </Badge>\n )}\n </div>\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n\n {/* Model info */}\n {selectedModel && (\n <div className=\"text-muted-foreground flex flex-wrap gap-2 text-xs\">\n <span>\n Context: {Math.round(selectedModel.contextWindow / 1000)}K tokens\n </span>\n {selectedModel.capabilities.vision && <span>• Vision</span>}\n {selectedModel.capabilities.tools && <span>• Tools</span>}\n {selectedModel.capabilities.reasoning && <span>• Reasoning</span>}\n </div>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { ChatContainer } from "./ChatContainer.js";
|
|
2
|
+
import { ChatMessage } from "./ChatMessage.js";
|
|
3
|
+
import { ChatInput } from "./ChatInput.js";
|
|
4
|
+
import { ModelPicker } from "./ModelPicker.js";
|
|
5
|
+
import { ContextIndicator } from "./ContextIndicator.js";
|
|
6
|
+
import { CodePreview } from "./CodePreview.js";
|
|
7
|
+
export { ChatContainer, ChatInput, ChatMessage, CodePreview, ContextIndicator, ModelPicker };
|
|
@@ -0,0 +1,8 @@
|
|
|
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 };
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { ChatAttachment, ChatConversation, ChatMessage } from "../../core/message-types.js";
|
|
2
|
+
import { ProviderMode, ProviderName } from "@contractspec/lib.ai-providers";
|
|
3
|
+
|
|
4
|
+
//#region src/presentation/hooks/useChat.d.ts
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Options for useChat hook
|
|
8
|
+
*/
|
|
9
|
+
interface UseChatOptions {
|
|
10
|
+
/** Provider to use */
|
|
11
|
+
provider?: ProviderName;
|
|
12
|
+
/** Provider mode */
|
|
13
|
+
mode?: ProviderMode;
|
|
14
|
+
/** Model to use */
|
|
15
|
+
model?: string;
|
|
16
|
+
/** API key for BYOK mode */
|
|
17
|
+
apiKey?: string;
|
|
18
|
+
/** API proxy URL for managed mode */
|
|
19
|
+
proxyUrl?: string;
|
|
20
|
+
/** Initial conversation ID to resume */
|
|
21
|
+
conversationId?: string;
|
|
22
|
+
/** System prompt override */
|
|
23
|
+
systemPrompt?: string;
|
|
24
|
+
/** Enable streaming */
|
|
25
|
+
streaming?: boolean;
|
|
26
|
+
/** Called when a message is sent */
|
|
27
|
+
onSend?: (message: ChatMessage) => void;
|
|
28
|
+
/** Called when a response is received */
|
|
29
|
+
onResponse?: (message: ChatMessage) => void;
|
|
30
|
+
/** Called on error */
|
|
31
|
+
onError?: (error: Error) => void;
|
|
32
|
+
/** Called when usage is recorded */
|
|
33
|
+
onUsage?: (usage: {
|
|
34
|
+
inputTokens: number;
|
|
35
|
+
outputTokens: number;
|
|
36
|
+
}) => void;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Return type for useChat hook
|
|
40
|
+
*/
|
|
41
|
+
interface UseChatReturn {
|
|
42
|
+
/** Current messages */
|
|
43
|
+
messages: ChatMessage[];
|
|
44
|
+
/** Current conversation */
|
|
45
|
+
conversation: ChatConversation | null;
|
|
46
|
+
/** Whether currently loading/streaming */
|
|
47
|
+
isLoading: boolean;
|
|
48
|
+
/** Current error */
|
|
49
|
+
error: Error | null;
|
|
50
|
+
/** Send a message */
|
|
51
|
+
sendMessage: (content: string, attachments?: ChatAttachment[]) => Promise<void>;
|
|
52
|
+
/** Clear conversation and start fresh */
|
|
53
|
+
clearConversation: () => void;
|
|
54
|
+
/** Set conversation ID to resume */
|
|
55
|
+
setConversationId: (id: string | null) => void;
|
|
56
|
+
/** Regenerate last response */
|
|
57
|
+
regenerate: () => Promise<void>;
|
|
58
|
+
/** Stop current generation */
|
|
59
|
+
stop: () => void;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Hook for managing AI chat state
|
|
63
|
+
*/
|
|
64
|
+
declare function useChat(options?: UseChatOptions): UseChatReturn;
|
|
65
|
+
//#endregion
|
|
66
|
+
export { UseChatOptions, UseChatReturn, useChat };
|
|
67
|
+
//# sourceMappingURL=useChat.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useChat.d.ts","names":[],"sources":["../../../src/presentation/hooks/useChat.tsx"],"sourcesContent":[],"mappings":";;;;;;;AAkBA;AAEa,UAFI,cAAA,CAEJ;EAEJ;EAcY,QAAA,CAAA,EAhBR,YAgBQ;EAEI;EAEL,IAAA,CAAA,EAlBX,YAkBW;EAAK;EAQR,KAAA,CAAA,EAAA,MAAA;EAEL;EAEI,MAAA,CAAA,EAAA,MAAA;EAIP;EAIS,QAAA,CAAA,EAAA,MAAA;EACX;EAMa,cAAA,CAAA,EAAA,MAAA;EAAO;EAQX,YAAO,CAAA,EAAA,MAAA;;;;qBAvCF;;yBAEI;;oBAEL;;;;;;;;;;UAQH,aAAA;;YAEL;;gBAEI;;;;SAIP;;+CAIS,qBACX;;;;;;oBAMa;;;;;;;iBAQJ,OAAA,WAAiB,iBAAsB"}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { ChatService } from "../../core/chat-service.js";
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
import { createProvider } from "@contractspec/lib.ai-providers";
|
|
6
|
+
|
|
7
|
+
//#region src/presentation/hooks/useChat.tsx
|
|
8
|
+
/**
|
|
9
|
+
* Hook for managing AI chat state
|
|
10
|
+
*/
|
|
11
|
+
function useChat(options = {}) {
|
|
12
|
+
const { provider = "openai", mode = "byok", model, apiKey, proxyUrl, conversationId: initialConversationId, systemPrompt, streaming = true, onSend, onResponse, onError, onUsage } = options;
|
|
13
|
+
const [messages, setMessages] = React.useState([]);
|
|
14
|
+
const [conversation, setConversation] = React.useState(null);
|
|
15
|
+
const [isLoading, setIsLoading] = React.useState(false);
|
|
16
|
+
const [error, setError] = React.useState(null);
|
|
17
|
+
const [conversationId, setConversationId] = React.useState(initialConversationId ?? null);
|
|
18
|
+
const abortControllerRef = React.useRef(null);
|
|
19
|
+
const chatServiceRef = React.useRef(null);
|
|
20
|
+
React.useEffect(() => {
|
|
21
|
+
chatServiceRef.current = new ChatService({
|
|
22
|
+
provider: createProvider({
|
|
23
|
+
provider,
|
|
24
|
+
model,
|
|
25
|
+
apiKey,
|
|
26
|
+
proxyUrl
|
|
27
|
+
}),
|
|
28
|
+
systemPrompt,
|
|
29
|
+
onUsage
|
|
30
|
+
});
|
|
31
|
+
}, [
|
|
32
|
+
provider,
|
|
33
|
+
mode,
|
|
34
|
+
model,
|
|
35
|
+
apiKey,
|
|
36
|
+
proxyUrl,
|
|
37
|
+
systemPrompt,
|
|
38
|
+
onUsage
|
|
39
|
+
]);
|
|
40
|
+
React.useEffect(() => {
|
|
41
|
+
if (!conversationId || !chatServiceRef.current) return;
|
|
42
|
+
const loadConversation = async () => {
|
|
43
|
+
if (!chatServiceRef.current) return;
|
|
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 };
|
|
172
|
+
//# sourceMappingURL=useChat.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useChat.js","names":["error"],"sources":["../../../src/presentation/hooks/useChat.tsx"],"sourcesContent":["'use client';\n\nimport * as React from 'react';\nimport type {\n ChatAttachment,\n ChatConversation,\n ChatMessage,\n} from '../../core/message-types';\nimport { ChatService } from '../../core/chat-service';\nimport {\n createProvider,\n type ProviderMode,\n type ProviderName,\n} from '@contractspec/lib.ai-providers';\n\n/**\n * Options for useChat hook\n */\nexport interface UseChatOptions {\n /** Provider to use */\n provider?: ProviderName;\n /** Provider mode */\n mode?: ProviderMode;\n /** Model to use */\n model?: string;\n /** API key for BYOK mode */\n apiKey?: string;\n /** API proxy URL for managed mode */\n proxyUrl?: string;\n /** Initial conversation ID to resume */\n conversationId?: string;\n /** System prompt override */\n systemPrompt?: string;\n /** Enable streaming */\n streaming?: boolean;\n /** Called when a message is sent */\n onSend?: (message: ChatMessage) => void;\n /** Called when a response is received */\n onResponse?: (message: ChatMessage) => void;\n /** Called on error */\n onError?: (error: Error) => void;\n /** Called when usage is recorded */\n onUsage?: (usage: { inputTokens: number; outputTokens: number }) => void;\n}\n\n/**\n * Return type for useChat hook\n */\nexport interface UseChatReturn {\n /** Current messages */\n messages: ChatMessage[];\n /** Current conversation */\n conversation: ChatConversation | null;\n /** Whether currently loading/streaming */\n isLoading: boolean;\n /** Current error */\n error: Error | null;\n /** Send a message */\n sendMessage: (\n content: string,\n attachments?: ChatAttachment[]\n ) => Promise<void>;\n /** Clear conversation and start fresh */\n clearConversation: () => void;\n /** Set conversation ID to resume */\n setConversationId: (id: string | null) => void;\n /** Regenerate last response */\n regenerate: () => Promise<void>;\n /** Stop current generation */\n stop: () => void;\n}\n\n/**\n * Hook for managing AI chat state\n */\nexport function useChat(options: UseChatOptions = {}): UseChatReturn {\n const {\n provider = 'openai',\n mode = 'byok',\n model,\n apiKey,\n proxyUrl,\n conversationId: initialConversationId,\n systemPrompt,\n streaming = true,\n onSend,\n onResponse,\n onError,\n onUsage,\n } = options;\n\n const [messages, setMessages] = React.useState<ChatMessage[]>([]);\n const [conversation, setConversation] =\n React.useState<ChatConversation | null>(null);\n const [isLoading, setIsLoading] = React.useState(false);\n const [error, setError] = React.useState<Error | null>(null);\n const [conversationId, setConversationId] = React.useState<string | null>(\n initialConversationId ?? null\n );\n\n const abortControllerRef = React.useRef<AbortController | null>(null);\n const chatServiceRef = React.useRef<ChatService | null>(null);\n\n // Initialize chat service\n React.useEffect(() => {\n const chatProvider = createProvider({\n provider,\n model,\n apiKey,\n proxyUrl,\n });\n\n chatServiceRef.current = new ChatService({\n provider: chatProvider,\n systemPrompt,\n onUsage,\n });\n }, [provider, mode, model, apiKey, proxyUrl, systemPrompt, onUsage]);\n\n // Load existing conversation\n React.useEffect(() => {\n if (!conversationId || !chatServiceRef.current) return;\n\n const loadConversation = async () => {\n if (!chatServiceRef.current) return;\n\n const conv = await chatServiceRef.current.getConversation(conversationId);\n if (conv) {\n setConversation(conv);\n setMessages(conv.messages);\n }\n };\n\n loadConversation().catch(console.error);\n }, [conversationId]);\n\n const sendMessage = React.useCallback(\n async (content: string, attachments?: ChatAttachment[]) => {\n if (!chatServiceRef.current) {\n throw new Error('Chat service not initialized');\n }\n\n setIsLoading(true);\n setError(null);\n\n // Create abort controller\n abortControllerRef.current = new AbortController();\n\n try {\n // Add user message immediately\n const userMessage: ChatMessage = {\n id: `msg_${Date.now()}`,\n conversationId: conversationId ?? '',\n role: 'user',\n content,\n status: 'completed',\n createdAt: new Date(),\n updatedAt: new Date(),\n attachments,\n };\n setMessages((prev) => [...prev, userMessage]);\n onSend?.(userMessage);\n\n if (streaming) {\n // Streaming mode\n const result = await chatServiceRef.current.stream({\n conversationId: conversationId ?? undefined,\n content,\n attachments,\n });\n\n // Update conversation ID if new\n if (!conversationId) {\n setConversationId(result.conversationId);\n }\n\n // Add placeholder for assistant message\n const assistantMessage: ChatMessage = {\n id: result.messageId,\n conversationId: result.conversationId,\n role: 'assistant',\n content: '',\n status: 'streaming',\n createdAt: new Date(),\n updatedAt: new Date(),\n };\n setMessages((prev) => [...prev, assistantMessage]);\n\n // Process stream\n let fullContent = '';\n for await (const chunk of result.stream) {\n if (chunk.type === 'text' && chunk.content) {\n fullContent += chunk.content;\n setMessages((prev) =>\n prev.map((m) =>\n m.id === result.messageId ? { ...m, content: fullContent } : m\n )\n );\n } else if (chunk.type === 'done') {\n setMessages((prev) =>\n prev.map((m) =>\n m.id === result.messageId\n ? {\n ...m,\n status: 'completed',\n usage: chunk.usage,\n updatedAt: new Date(),\n }\n : m\n )\n );\n onResponse?.(\n messages.find((m) => m.id === result.messageId) ??\n assistantMessage\n );\n } else if (chunk.type === 'error') {\n setMessages((prev) =>\n prev.map((m) =>\n m.id === result.messageId\n ? {\n ...m,\n status: 'error',\n error: chunk.error,\n updatedAt: new Date(),\n }\n : m\n )\n );\n if (chunk.error) {\n const err = new Error(chunk.error.message);\n setError(err);\n onError?.(err);\n }\n }\n }\n } else {\n // Non-streaming mode\n const result = await chatServiceRef.current.send({\n conversationId: conversationId ?? undefined,\n content,\n attachments,\n });\n\n setConversation(result.conversation);\n setMessages(result.conversation.messages);\n\n if (!conversationId) {\n setConversationId(result.conversation.id);\n }\n\n onResponse?.(result.message);\n }\n } catch (err) {\n const error = err instanceof Error ? err : new Error(String(err));\n setError(error);\n onError?.(error);\n } finally {\n setIsLoading(false);\n abortControllerRef.current = null;\n }\n },\n [conversationId, streaming, onSend, onResponse, onError, messages]\n );\n\n const clearConversation = React.useCallback(() => {\n setMessages([]);\n setConversation(null);\n setConversationId(null);\n setError(null);\n }, []);\n\n const regenerate = React.useCallback(async () => {\n // Find the last user message\n const lastUserMessageIndex = messages.findLastIndex(\n (m) => m.role === 'user'\n );\n if (lastUserMessageIndex === -1) return;\n\n const lastUserMessage = messages[lastUserMessageIndex];\n if (!lastUserMessage) return;\n\n // Remove the last assistant message\n setMessages((prev) => prev.slice(0, lastUserMessageIndex + 1));\n\n // Resend\n await sendMessage(lastUserMessage.content, lastUserMessage.attachments);\n }, [messages, sendMessage]);\n\n const stop = React.useCallback(() => {\n abortControllerRef.current?.abort();\n setIsLoading(false);\n }, []);\n\n return {\n messages,\n conversation,\n isLoading,\n error,\n sendMessage,\n clearConversation,\n setConversationId,\n regenerate,\n stop,\n };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { ModelInfo, ProviderMode, ProviderName } from "@contractspec/lib.ai-providers";
|
|
2
|
+
|
|
3
|
+
//#region src/presentation/hooks/useProviders.d.ts
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Provider availability info
|
|
7
|
+
*/
|
|
8
|
+
interface ProviderInfo {
|
|
9
|
+
provider: ProviderName;
|
|
10
|
+
available: boolean;
|
|
11
|
+
mode: ProviderMode;
|
|
12
|
+
reason?: string;
|
|
13
|
+
models: ModelInfo[];
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Return type for useProviders hook
|
|
17
|
+
*/
|
|
18
|
+
interface UseProvidersReturn {
|
|
19
|
+
/** All providers with availability info */
|
|
20
|
+
providers: ProviderInfo[];
|
|
21
|
+
/** Available providers only */
|
|
22
|
+
availableProviders: ProviderInfo[];
|
|
23
|
+
/** Check if a provider is available */
|
|
24
|
+
isAvailable: (provider: ProviderName) => boolean;
|
|
25
|
+
/** Get models for a provider */
|
|
26
|
+
getModels: (provider: ProviderName) => ModelInfo[];
|
|
27
|
+
/** Loading state */
|
|
28
|
+
isLoading: boolean;
|
|
29
|
+
/** Refresh provider availability */
|
|
30
|
+
refresh: () => Promise<void>;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Hook for managing AI provider information
|
|
34
|
+
*/
|
|
35
|
+
declare function useProviders(): UseProvidersReturn;
|
|
36
|
+
//#endregion
|
|
37
|
+
export { UseProvidersReturn, useProviders };
|
|
38
|
+
//# sourceMappingURL=useProviders.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useProviders.d.ts","names":[],"sources":["../../../src/presentation/hooks/useProviders.tsx"],"sourcesContent":[],"mappings":";;;;;;AAcA;AACY,UADK,YAAA,CACL;EAEJ,QAAA,EAFI,YAEJ;EAEE,SAAA,EAAA,OAAA;EAAS,IAAA,EAFX,YAEW;EAMF,MAAA,CAAA,EAAA,MAAA;EAEJ,MAAA,EARH,SAQG,EAAA;;;;;AAUI,UAZA,kBAAA,CAYA;EAAO;EAMR,SAAA,EAhBH,YAgBe,EAAA;;sBAdN;;0BAEI;;wBAEF,iBAAiB;;;;iBAIxB;;;;;iBAMD,YAAA,CAAA,GAAgB"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { getAvailableProviders, getModelsForProvider } from "@contractspec/lib.ai-providers";
|
|
5
|
+
|
|
6
|
+
//#region src/presentation/hooks/useProviders.tsx
|
|
7
|
+
/**
|
|
8
|
+
* Hook for managing AI provider information
|
|
9
|
+
*/
|
|
10
|
+
function useProviders() {
|
|
11
|
+
const [providers, setProviders] = React.useState([]);
|
|
12
|
+
const [isLoading, setIsLoading] = React.useState(true);
|
|
13
|
+
const loadProviders = React.useCallback(async () => {
|
|
14
|
+
setIsLoading(true);
|
|
15
|
+
try {
|
|
16
|
+
setProviders(getAvailableProviders().map((p) => ({
|
|
17
|
+
...p,
|
|
18
|
+
models: getModelsForProvider(p.provider)
|
|
19
|
+
})));
|
|
20
|
+
} catch (error) {
|
|
21
|
+
console.error("Failed to load providers:", error);
|
|
22
|
+
} finally {
|
|
23
|
+
setIsLoading(false);
|
|
24
|
+
}
|
|
25
|
+
}, []);
|
|
26
|
+
React.useEffect(() => {
|
|
27
|
+
loadProviders();
|
|
28
|
+
}, [loadProviders]);
|
|
29
|
+
return {
|
|
30
|
+
providers,
|
|
31
|
+
availableProviders: React.useMemo(() => providers.filter((p) => p.available), [providers]),
|
|
32
|
+
isAvailable: React.useCallback((provider) => providers.some((p) => p.provider === provider && p.available), [providers]),
|
|
33
|
+
getModels: React.useCallback((provider) => providers.find((p) => p.provider === provider)?.models ?? [], [providers]),
|
|
34
|
+
isLoading,
|
|
35
|
+
refresh: loadProviders
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
//#endregion
|
|
40
|
+
export { useProviders };
|
|
41
|
+
//# sourceMappingURL=useProviders.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useProviders.js","names":[],"sources":["../../../src/presentation/hooks/useProviders.tsx"],"sourcesContent":["'use client';\n\nimport * as React from 'react';\nimport {\n type ProviderName,\n type ProviderMode,\n type ModelInfo,\n getAvailableProviders,\n getModelsForProvider,\n} from '@contractspec/lib.ai-providers';\n\n/**\n * Provider availability info\n */\nexport interface ProviderInfo {\n provider: ProviderName;\n available: boolean;\n mode: ProviderMode;\n reason?: string;\n models: ModelInfo[];\n}\n\n/**\n * Return type for useProviders hook\n */\nexport interface UseProvidersReturn {\n /** All providers with availability info */\n providers: ProviderInfo[];\n /** Available providers only */\n availableProviders: ProviderInfo[];\n /** Check if a provider is available */\n isAvailable: (provider: ProviderName) => boolean;\n /** Get models for a provider */\n getModels: (provider: ProviderName) => ModelInfo[];\n /** Loading state */\n isLoading: boolean;\n /** Refresh provider availability */\n refresh: () => Promise<void>;\n}\n\n/**\n * Hook for managing AI provider information\n */\nexport function useProviders(): UseProvidersReturn {\n const [providers, setProviders] = React.useState<ProviderInfo[]>([]);\n const [isLoading, setIsLoading] = React.useState(true);\n\n const loadProviders = React.useCallback(async () => {\n setIsLoading(true);\n try {\n const available = getAvailableProviders();\n const providersWithModels: ProviderInfo[] = available.map((p) => ({\n ...p,\n models: getModelsForProvider(p.provider),\n }));\n setProviders(providersWithModels);\n } catch (error) {\n console.error('Failed to load providers:', error);\n } finally {\n setIsLoading(false);\n }\n }, []);\n\n React.useEffect(() => {\n loadProviders();\n }, [loadProviders]);\n\n const availableProviders = React.useMemo(\n () => providers.filter((p) => p.available),\n [providers]\n );\n\n const isAvailable = React.useCallback(\n (provider: ProviderName) =>\n providers.some((p) => p.provider === provider && p.available),\n [providers]\n );\n\n const getModelsCallback = React.useCallback(\n (provider: ProviderName) =>\n providers.find((p) => p.provider === provider)?.models ?? [],\n [providers]\n );\n\n return {\n providers,\n availableProviders,\n isAvailable,\n getModels: getModelsCallback,\n isLoading,\n refresh: loadProviders,\n };\n}\n"],"mappings":";;;;;;;;;AA2CA,SAAgB,eAAmC;CACjD,MAAM,CAAC,WAAW,gBAAgB,MAAM,SAAyB,EAAE,CAAC;CACpE,MAAM,CAAC,WAAW,gBAAgB,MAAM,SAAS,KAAK;CAEtD,MAAM,gBAAgB,MAAM,YAAY,YAAY;AAClD,eAAa,KAAK;AAClB,MAAI;AAMF,gBALkB,uBAAuB,CACa,KAAK,OAAO;IAChE,GAAG;IACH,QAAQ,qBAAqB,EAAE,SAAS;IACzC,EAAE,CAC8B;WAC1B,OAAO;AACd,WAAQ,MAAM,6BAA6B,MAAM;YACzC;AACR,gBAAa,MAAM;;IAEpB,EAAE,CAAC;AAEN,OAAM,gBAAgB;AACpB,iBAAe;IACd,CAAC,cAAc,CAAC;AAmBnB,QAAO;EACL;EACA,oBAnByB,MAAM,cACzB,UAAU,QAAQ,MAAM,EAAE,UAAU,EAC1C,CAAC,UAAU,CACZ;EAiBC,aAfkB,MAAM,aACvB,aACC,UAAU,MAAM,MAAM,EAAE,aAAa,YAAY,EAAE,UAAU,EAC/D,CAAC,UAAU,CACZ;EAYC,WAVwB,MAAM,aAC7B,aACC,UAAU,MAAM,MAAM,EAAE,aAAa,SAAS,EAAE,UAAU,EAAE,EAC9D,CAAC,UAAU,CACZ;EAOC;EACA,SAAS;EACV"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { ChatContainer } from "./components/ChatContainer.js";
|
|
2
|
+
import { ChatMessage } from "./components/ChatMessage.js";
|
|
3
|
+
import { ChatInput } from "./components/ChatInput.js";
|
|
4
|
+
import { ModelPicker } from "./components/ModelPicker.js";
|
|
5
|
+
import { ContextIndicator } from "./components/ContextIndicator.js";
|
|
6
|
+
import { CodePreview } from "./components/CodePreview.js";
|
|
7
|
+
import "./components/index.js";
|
|
8
|
+
import { UseChatOptions, UseChatReturn, useChat } from "./hooks/useChat.js";
|
|
9
|
+
import { UseProvidersReturn, useProviders } from "./hooks/useProviders.js";
|
|
10
|
+
import "./hooks/index.js";
|
|
11
|
+
export { ChatContainer, ChatInput, ChatMessage, CodePreview, ContextIndicator, ModelPicker, UseChatOptions, UseChatReturn, UseProvidersReturn, useChat, useProviders };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { ChatContainer } from "./components/ChatContainer.js";
|
|
2
|
+
import { CodePreview } from "./components/CodePreview.js";
|
|
3
|
+
import { ChatMessage } from "./components/ChatMessage.js";
|
|
4
|
+
import { ChatInput } from "./components/ChatInput.js";
|
|
5
|
+
import { ModelPicker } from "./components/ModelPicker.js";
|
|
6
|
+
import { ContextIndicator } from "./components/ContextIndicator.js";
|
|
7
|
+
import "./components/index.js";
|
|
8
|
+
import { useChat } from "./hooks/useChat.js";
|
|
9
|
+
import { useProviders } from "./hooks/useProviders.js";
|
|
10
|
+
import "./hooks/index.js";
|
|
11
|
+
|
|
12
|
+
export { ChatContainer, ChatInput, ChatMessage, CodePreview, ContextIndicator, ModelPicker, useChat, useProviders };
|