@bibobonking/opensol 3.1.2 → 3.1.3
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/CHANGELOG.md +1 -1
- package/dist/assets/index-DaJcH9ts.css +1 -0
- package/dist/assets/index-hs0KWMP6.js +288 -0
- package/dist/favicon.svg +11 -5
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/public/favicon.svg +11 -5
- package/server/daemon.mjs +42 -4
- package/server/lib/deploy.mjs +78 -7
- package/server/lib/skills.mjs +128 -0
- package/server/lib/tools.mjs +40 -0
- package/src/agent/adapters/openai-responses.ts +5 -1
- package/src/agent/adapters/openai.ts +15 -2
- package/src/agent/prompt.ts +4 -7
- package/src/agent/tools.ts +1 -0
- package/src/components/DeployModal.tsx +77 -15
- package/src/components/FileIcon.tsx +108 -0
- package/src/components/HomeDashboard.tsx +9 -2
- package/src/components/Logo.tsx +11 -5
- package/src/components/ModelDeck.tsx +12 -2
- package/src/components/SettingsModal.tsx +144 -199
- package/src/components/Sidebar.tsx +4 -5
- package/src/components/SystemPromptModal.tsx +168 -0
- package/src/components/TopBar.tsx +19 -0
- package/src/components/WorkspacePanel.tsx +10 -8
- package/src/core/kernel.ts +2 -0
- package/src/core/plugins/tools/skill.ts +101 -0
- package/src/core/services/agent-loop.ts +10 -22
- package/src/core/services/system-prompt.ts +15 -13
- package/src/core/services/tools.ts +1 -0
- package/src/lib/daemon.ts +19 -0
- package/src/lib/permissions.ts +1 -1
- package/src/lib/skills.test.mjs +22 -0
- package/src/lib/skills.ts +93 -0
- package/src/lib/usage.ts +12 -0
- package/src/store/chats.ts +1 -0
- package/src/store/settings.ts +21 -9
- package/src/types.ts +14 -1
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/assets/index-CeN54A2N.css +0 -1
- package/dist/assets/index-fhIO9a7_.js +0 -318
- package/src/data/skills.ts +0 -68
|
@@ -9,12 +9,14 @@ import {
|
|
|
9
9
|
Play,
|
|
10
10
|
Rocket,
|
|
11
11
|
ShieldCheck,
|
|
12
|
+
Sparkles,
|
|
12
13
|
Square,
|
|
13
14
|
Triangle,
|
|
14
15
|
Terminal,
|
|
16
|
+
X,
|
|
15
17
|
} from 'lucide-react'
|
|
16
18
|
import type { DeployProviderId, DeployResult } from '../types'
|
|
17
|
-
import { streamDeploy } from '../lib/daemon'
|
|
19
|
+
import { inspectDeploy, streamDeploy, type DeployInspection } from '../lib/daemon'
|
|
18
20
|
import { cn } from '../lib/utils'
|
|
19
21
|
import { useSettings } from '../store/settings'
|
|
20
22
|
import { useUi } from '../store/ui'
|
|
@@ -74,9 +76,11 @@ export function DeployModal() {
|
|
|
74
76
|
|
|
75
77
|
const [provider, setProvider] = useState<DeployProviderId>('cloudflare')
|
|
76
78
|
const [path, setPath] = useState('')
|
|
77
|
-
const [outputDir, setOutputDir] = useState('
|
|
79
|
+
const [outputDir, setOutputDir] = useState('')
|
|
78
80
|
const [buildCommand, setBuildCommand] = useState('')
|
|
79
81
|
const [projectName, setProjectName] = useState('')
|
|
82
|
+
const [inspection, setInspection] = useState<DeployInspection | null>(null)
|
|
83
|
+
const [inspecting, setInspecting] = useState(false)
|
|
80
84
|
const [running, setRunning] = useState(false)
|
|
81
85
|
const [logs, setLogs] = useState<LogLine[]>([])
|
|
82
86
|
const [result, setResult] = useState<DeployResult | null>(null)
|
|
@@ -89,10 +93,15 @@ export function DeployModal() {
|
|
|
89
93
|
const meta = PROVIDERS.find((entry) => entry.id === provider) ?? PROVIDERS[0]
|
|
90
94
|
const tool = deployTools?.[provider]
|
|
91
95
|
const configured = Boolean(deploySecrets?.[provider]?.configured)
|
|
96
|
+
const lastUrl = deploySecrets?.[provider]?.fields?.lastUrl ?? ''
|
|
92
97
|
|
|
93
98
|
useEffect(() => {
|
|
94
99
|
if (!open) return
|
|
95
100
|
setPath(workspaceRoot)
|
|
101
|
+
setOutputDir('')
|
|
102
|
+
setBuildCommand('')
|
|
103
|
+
setProjectName('')
|
|
104
|
+
setInspection(null)
|
|
96
105
|
setResult(null)
|
|
97
106
|
setError('')
|
|
98
107
|
setLogs([])
|
|
@@ -100,6 +109,30 @@ export function DeployModal() {
|
|
|
100
109
|
setCopied(false)
|
|
101
110
|
}, [open, workspaceRoot])
|
|
102
111
|
|
|
112
|
+
const detect = async (target = path) => {
|
|
113
|
+
if (!target.trim()) return
|
|
114
|
+
setInspecting(true)
|
|
115
|
+
try {
|
|
116
|
+
const info = await inspectDeploy(target.trim())
|
|
117
|
+
setInspection(info)
|
|
118
|
+
if (info.buildCommand) setBuildCommand((value) => value || info.buildCommand)
|
|
119
|
+
if (info.outputDir) setOutputDir((value) => value || info.outputDir)
|
|
120
|
+
if (info.name) setProjectName((value) => value || info.name)
|
|
121
|
+
} catch (cause) {
|
|
122
|
+
setInspection(null)
|
|
123
|
+
setError(cause instanceof Error ? cause.message : 'Could not inspect that folder.')
|
|
124
|
+
} finally {
|
|
125
|
+
setInspecting(false)
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Fill the form from the real project once the path settles.
|
|
130
|
+
useEffect(() => {
|
|
131
|
+
if (!open || !path.trim()) return
|
|
132
|
+
const timer = window.setTimeout(() => void detect(path), 450)
|
|
133
|
+
return () => window.clearTimeout(timer)
|
|
134
|
+
}, [open, path])
|
|
135
|
+
|
|
103
136
|
useEffect(() => {
|
|
104
137
|
if (!open) return
|
|
105
138
|
const node = logRef.current
|
|
@@ -201,7 +234,7 @@ export function DeployModal() {
|
|
|
201
234
|
</div>
|
|
202
235
|
</div>
|
|
203
236
|
<button type="button" className="icon-button -mr-1 shrink-0" onClick={close} aria-label="Close deploy panel">
|
|
204
|
-
<
|
|
237
|
+
<X size={16} />
|
|
205
238
|
</button>
|
|
206
239
|
</div>
|
|
207
240
|
|
|
@@ -221,16 +254,16 @@ export function DeployModal() {
|
|
|
221
254
|
onClick={() => setProvider(entry.id)}
|
|
222
255
|
disabled={running}
|
|
223
256
|
className={cn(
|
|
224
|
-
'flex items-start gap-2.5 rounded-xl border px-3 py-2.5 text-left transition-
|
|
257
|
+
'group/host flex items-start gap-2.5 rounded-xl border px-3 py-2.5 text-left transition-all duration-200 disabled:opacity-60',
|
|
225
258
|
active
|
|
226
|
-
? 'border-accent/
|
|
227
|
-
: 'border-line bg-panel hover:bg-raised/50',
|
|
259
|
+
? 'border-accent/40 bg-gradient-to-br from-accent-soft/80 to-panel shadow-pill'
|
|
260
|
+
: 'border-line bg-panel hover:border-line hover:bg-raised/50',
|
|
228
261
|
)}
|
|
229
262
|
>
|
|
230
263
|
<span
|
|
231
264
|
className={cn(
|
|
232
|
-
'mt-0.5 flex h-7 w-7 shrink-0 items-center justify-center rounded-lg',
|
|
233
|
-
active ? 'bg-accent text-on-accent' : 'bg-raised text-muted',
|
|
265
|
+
'mt-0.5 flex h-7 w-7 shrink-0 items-center justify-center rounded-lg transition-colors',
|
|
266
|
+
active ? 'bg-accent text-on-accent shadow-pill' : 'bg-raised text-muted group-hover/host:text-ink',
|
|
234
267
|
)}
|
|
235
268
|
>
|
|
236
269
|
<Icon size={13} />
|
|
@@ -254,6 +287,18 @@ export function DeployModal() {
|
|
|
254
287
|
</div>
|
|
255
288
|
</div>
|
|
256
289
|
|
|
290
|
+
{lastUrl && (
|
|
291
|
+
<a
|
|
292
|
+
href={lastUrl}
|
|
293
|
+
target="_blank"
|
|
294
|
+
rel="noreferrer"
|
|
295
|
+
className="flex items-center gap-1.5 rounded-lg border border-line bg-raised/30 px-2.5 py-1.5 text-[9.5px] text-muted hover:text-ink"
|
|
296
|
+
>
|
|
297
|
+
<ExternalLink size={10} className="shrink-0 text-faint" />
|
|
298
|
+
<span className="truncate">Last deploy: {lastUrl}</span>
|
|
299
|
+
</a>
|
|
300
|
+
)}
|
|
301
|
+
|
|
257
302
|
{!configured && (
|
|
258
303
|
<div className="rounded-xl border border-warning/25 bg-warning/[0.045] p-3">
|
|
259
304
|
<p className="flex items-start gap-1.5 text-[10.5px] leading-relaxed text-muted">
|
|
@@ -300,10 +345,27 @@ export function DeployModal() {
|
|
|
300
345
|
spellCheck={false}
|
|
301
346
|
className="h-9 w-full rounded-lg border border-line bg-panel px-2.5 font-mono text-[10.5px] text-ink outline-none transition-colors placeholder:text-faint focus:border-accent/50 disabled:opacity-60"
|
|
302
347
|
/>
|
|
303
|
-
<
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
348
|
+
<div className="mt-1.5 flex items-center gap-2">
|
|
349
|
+
<p className="min-w-0 flex-1 truncate text-[9px] leading-relaxed text-faint">
|
|
350
|
+
{workspaceAvailable ? `Open workspace: ${workspaceName}` : 'No workspace open.'} Any folder that already exists on this machine.
|
|
351
|
+
</p>
|
|
352
|
+
<button
|
|
353
|
+
type="button"
|
|
354
|
+
onClick={() => void detect()}
|
|
355
|
+
disabled={running || inspecting || !path.trim()}
|
|
356
|
+
className="inline-flex h-6 shrink-0 items-center gap-1 rounded-md border border-line bg-panel px-1.5 text-[9px] font-medium text-muted hover:text-ink disabled:opacity-40"
|
|
357
|
+
>
|
|
358
|
+
{inspecting ? <Spinner size={9} /> : <Sparkles size={9} />}
|
|
359
|
+
Detect
|
|
360
|
+
</button>
|
|
361
|
+
</div>
|
|
362
|
+
{inspection && (
|
|
363
|
+
<div className="mt-2 flex flex-wrap items-center gap-1.5 rounded-lg border border-line bg-raised/40 px-2 py-1.5 text-[9px] text-muted">
|
|
364
|
+
<span className="rounded bg-panel px-1.5 py-0.5 font-medium text-ink">{inspection.framework}</span>
|
|
365
|
+
{inspection.existingOutputs.length > 0 && <span>build output: {inspection.existingOutputs.join(', ')}</span>}
|
|
366
|
+
{!inspection.hasPackageJson && <span>static folder, no package.json</span>}
|
|
367
|
+
</div>
|
|
368
|
+
)}
|
|
307
369
|
</div>
|
|
308
370
|
|
|
309
371
|
<div className="grid grid-cols-2 gap-2.5">
|
|
@@ -316,7 +378,7 @@ export function DeployModal() {
|
|
|
316
378
|
value={outputDir}
|
|
317
379
|
onChange={(event) => setOutputDir(event.target.value)}
|
|
318
380
|
disabled={running}
|
|
319
|
-
placeholder="
|
|
381
|
+
placeholder="auto"
|
|
320
382
|
spellCheck={false}
|
|
321
383
|
className="h-8 w-full rounded-lg border border-line bg-panel px-2 font-mono text-[10.5px] text-ink outline-none focus:border-accent/50 disabled:opacity-60"
|
|
322
384
|
/>
|
|
@@ -338,13 +400,13 @@ export function DeployModal() {
|
|
|
338
400
|
</div>
|
|
339
401
|
|
|
340
402
|
<div className="flex flex-wrap gap-1">
|
|
341
|
-
{['dist', 'build', 'out', '.'].map((value) => (
|
|
403
|
+
{['auto', 'dist', 'build', 'out', '.'].map((value) => (
|
|
342
404
|
<button
|
|
343
405
|
key={value}
|
|
344
406
|
type="button"
|
|
345
407
|
onClick={() => setOutputDir(value)}
|
|
346
408
|
disabled={running}
|
|
347
|
-
className={cn('chip h-7 px-2 text-[9.5px]', outputDir === value && 'chip-active')}
|
|
409
|
+
className={cn('chip h-7 px-2 text-[9.5px]', (outputDir || 'auto') === value && 'chip-active')}
|
|
348
410
|
>
|
|
349
411
|
{value}
|
|
350
412
|
</button>
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { LucideIcon } from 'lucide-react'
|
|
2
|
+
import {
|
|
3
|
+
Braces,
|
|
4
|
+
FileArchive,
|
|
5
|
+
FileAudio,
|
|
6
|
+
FileCode2,
|
|
7
|
+
FileCog,
|
|
8
|
+
FileImage,
|
|
9
|
+
FileKey2,
|
|
10
|
+
FileLock2,
|
|
11
|
+
FileText,
|
|
12
|
+
FileVideo,
|
|
13
|
+
Folder,
|
|
14
|
+
FolderOpen,
|
|
15
|
+
Palette,
|
|
16
|
+
Terminal,
|
|
17
|
+
} from 'lucide-react'
|
|
18
|
+
import { cn } from '../lib/utils'
|
|
19
|
+
|
|
20
|
+
const TYPES: Record<string, { icon: LucideIcon; tone: string }> = {
|
|
21
|
+
ts: { icon: FileCode2, tone: 'text-sky-500' },
|
|
22
|
+
tsx: { icon: FileCode2, tone: 'text-sky-500' },
|
|
23
|
+
js: { icon: FileCode2, tone: 'text-amber-400' },
|
|
24
|
+
jsx: { icon: FileCode2, tone: 'text-amber-400' },
|
|
25
|
+
mjs: { icon: FileCode2, tone: 'text-amber-400' },
|
|
26
|
+
cjs: { icon: FileCode2, tone: 'text-amber-400' },
|
|
27
|
+
rs: { icon: FileCode2, tone: 'text-orange-400' },
|
|
28
|
+
go: { icon: FileCode2, tone: 'text-cyan-400' },
|
|
29
|
+
py: { icon: FileCode2, tone: 'text-emerald-400' },
|
|
30
|
+
rb: { icon: FileCode2, tone: 'text-rose-400' },
|
|
31
|
+
java: { icon: FileCode2, tone: 'text-red-400' },
|
|
32
|
+
css: { icon: Palette, tone: 'text-fuchsia-400' },
|
|
33
|
+
scss: { icon: Palette, tone: 'text-pink-400' },
|
|
34
|
+
sass: { icon: Palette, tone: 'text-pink-400' },
|
|
35
|
+
less: { icon: Palette, tone: 'text-pink-400' },
|
|
36
|
+
html: { icon: FileCode2, tone: 'text-orange-500' },
|
|
37
|
+
vue: { icon: FileCode2, tone: 'text-emerald-500' },
|
|
38
|
+
svelte: { icon: FileCode2, tone: 'text-orange-500' },
|
|
39
|
+
json: { icon: Braces, tone: 'text-yellow-500' },
|
|
40
|
+
jsonc: { icon: Braces, tone: 'text-yellow-500' },
|
|
41
|
+
yaml: { icon: FileCog, tone: 'text-teal-400' },
|
|
42
|
+
yml: { icon: FileCog, tone: 'text-teal-400' },
|
|
43
|
+
toml: { icon: FileCog, tone: 'text-teal-400' },
|
|
44
|
+
ini: { icon: FileCog, tone: 'text-teal-400' },
|
|
45
|
+
env: { icon: FileKey2, tone: 'text-amber-500' },
|
|
46
|
+
md: { icon: FileText, tone: 'text-violet-400' },
|
|
47
|
+
mdx: { icon: FileText, tone: 'text-violet-400' },
|
|
48
|
+
txt: { icon: FileText, tone: 'text-slate-400' },
|
|
49
|
+
csv: { icon: FileText, tone: 'text-emerald-400' },
|
|
50
|
+
svg: { icon: FileImage, tone: 'text-purple-400' },
|
|
51
|
+
png: { icon: FileImage, tone: 'text-purple-400' },
|
|
52
|
+
jpg: { icon: FileImage, tone: 'text-purple-400' },
|
|
53
|
+
jpeg: { icon: FileImage, tone: 'text-purple-400' },
|
|
54
|
+
gif: { icon: FileImage, tone: 'text-purple-400' },
|
|
55
|
+
webp: { icon: FileImage, tone: 'text-purple-400' },
|
|
56
|
+
ico: { icon: FileImage, tone: 'text-purple-400' },
|
|
57
|
+
mp3: { icon: FileAudio, tone: 'text-rose-400' },
|
|
58
|
+
wav: { icon: FileAudio, tone: 'text-rose-400' },
|
|
59
|
+
mp4: { icon: FileVideo, tone: 'text-rose-400' },
|
|
60
|
+
mov: { icon: FileVideo, tone: 'text-rose-400' },
|
|
61
|
+
zip: { icon: FileArchive, tone: 'text-amber-500' },
|
|
62
|
+
gz: { icon: FileArchive, tone: 'text-amber-500' },
|
|
63
|
+
tar: { icon: FileArchive, tone: 'text-amber-500' },
|
|
64
|
+
lock: { icon: FileLock2, tone: 'text-slate-400' },
|
|
65
|
+
sh: { icon: Terminal, tone: 'text-emerald-400' },
|
|
66
|
+
bash: { icon: Terminal, tone: 'text-emerald-400' },
|
|
67
|
+
zsh: { icon: Terminal, tone: 'text-emerald-400' },
|
|
68
|
+
sql: { icon: FileText, tone: 'text-cyan-400' },
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const SPECIAL: Array<{ match: RegExp; icon: LucideIcon; tone: string }> = [
|
|
72
|
+
{ match: /^(dockerfile|makefile|justfile)$/i, icon: FileCog, tone: 'text-sky-400' },
|
|
73
|
+
{ match: /^\.env/i, icon: FileKey2, tone: 'text-amber-500' },
|
|
74
|
+
{ match: /^(\.gitignore|\.gitattributes|\.npmignore|\.dockerignore)$/i, icon: FileCog, tone: 'text-slate-400' },
|
|
75
|
+
{ match: /^package(-lock)?\.json$/i, icon: Braces, tone: 'text-emerald-400' },
|
|
76
|
+
{ match: /^(vite|webpack|rollup|tsconfig|tailwind|postcss)\./i, icon: FileCog, tone: 'text-sky-400' },
|
|
77
|
+
{ match: /^readme/i, icon: FileText, tone: 'text-violet-400' },
|
|
78
|
+
{ match: /^skill\.md$/i, icon: FileText, tone: 'text-accent' },
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
export function fileTone(path: string): { icon: LucideIcon; tone: string } {
|
|
82
|
+
const name = path.split('/').pop() ?? path
|
|
83
|
+
const special = SPECIAL.find((entry) => entry.match.test(name))
|
|
84
|
+
if (special) return { icon: special.icon, tone: special.tone }
|
|
85
|
+
const extension = /\.([a-z0-9]+)$/i.exec(name)?.[1]?.toLowerCase() ?? ''
|
|
86
|
+
return TYPES[extension] ?? { icon: FileText, tone: 'text-faint' }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function FileIcon({
|
|
90
|
+
path,
|
|
91
|
+
type = 'file',
|
|
92
|
+
open = false,
|
|
93
|
+
size = 13,
|
|
94
|
+
className,
|
|
95
|
+
}: {
|
|
96
|
+
path: string
|
|
97
|
+
type?: 'file' | 'dir' | 'link'
|
|
98
|
+
open?: boolean
|
|
99
|
+
size?: number
|
|
100
|
+
className?: string
|
|
101
|
+
}) {
|
|
102
|
+
if (type === 'dir') {
|
|
103
|
+
const Icon = open ? FolderOpen : Folder
|
|
104
|
+
return <Icon size={size} className={cn('shrink-0 fill-amber-400/25 text-amber-500', className)} />
|
|
105
|
+
}
|
|
106
|
+
const { icon: Icon, tone } = fileTone(path)
|
|
107
|
+
return <Icon size={size} className={cn('shrink-0', tone, className)} />
|
|
108
|
+
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { useMemo, useState } from 'react'
|
|
2
|
-
import { Activity, CalendarDays, Clock3, Cpu, MessageSquare, Sparkles } from 'lucide-react'
|
|
2
|
+
import { Activity, CalendarDays, Clock3, Cpu, CreditCard, MessageSquare, Sparkles } from 'lucide-react'
|
|
3
3
|
import { useChats } from '../store/chats'
|
|
4
4
|
import { useSettings } from '../store/settings'
|
|
5
5
|
import { useWorkspace } from '../store/workspace'
|
|
6
6
|
import { cn } from '../lib/utils'
|
|
7
|
-
import { computeUsage, formatTokens, shortModelName, type UsageRange } from '../lib/usage'
|
|
7
|
+
import { computeUsage, formatCredits, formatTokens, shortModelName, type UsageRange } from '../lib/usage'
|
|
8
8
|
import { PixleSol } from './PixleSol'
|
|
9
9
|
|
|
10
10
|
const RANGES: Array<{ value: UsageRange; label: string }> = [
|
|
@@ -118,6 +118,7 @@ export function HomeDashboard() {
|
|
|
118
118
|
<Stat icon={Activity} label="Sessions" value={String(stats.sessions)} />
|
|
119
119
|
<Stat icon={MessageSquare} label="Messages" value={String(stats.messages)} />
|
|
120
120
|
<Stat icon={Sparkles} label="Total tokens" value={stats.tokens ? formatTokens(stats.tokens) : '0'} />
|
|
121
|
+
<Stat icon={CreditCard} label="Credits spent" value={formatCredits(stats.credits)} />
|
|
121
122
|
<Stat icon={CalendarDays} label="Active days" value={String(stats.activeDays)} />
|
|
122
123
|
<Stat icon={Clock3} label="Peak hour" value={stats.peakHour} />
|
|
123
124
|
<Stat icon={Cpu} label="Favorite model" value={shortModelName(stats.favoriteModel)} />
|
|
@@ -143,6 +144,12 @@ export function HomeDashboard() {
|
|
|
143
144
|
{stats.messages === 0
|
|
144
145
|
? 'No activity in this range yet. Send a message to start the graph.'
|
|
145
146
|
: `You've used ~${formatTokens(stats.tokens)} tokens across ${stats.messages} messages.`}
|
|
147
|
+
{stats.credits > 0 && (
|
|
148
|
+
<>
|
|
149
|
+
<span className="px-1.5 text-line">·</span>
|
|
150
|
+
{formatCredits(stats.credits)} in provider credits
|
|
151
|
+
</>
|
|
152
|
+
)}
|
|
146
153
|
{stats.favoriteModel !== '—' && (
|
|
147
154
|
<>
|
|
148
155
|
<span className="px-1.5 text-line">·</span>
|
package/src/components/Logo.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { cn } from '../lib/utils'
|
|
2
2
|
|
|
3
|
-
/** The OpenSol
|
|
3
|
+
/** The OpenSol aperture: a broken solar ring around a solid core. Inherits `currentColor`. */
|
|
4
4
|
export function LogoMark({ size = 28, className, spin = false }: { size?: number; className?: string; spin?: boolean }) {
|
|
5
5
|
return (
|
|
6
6
|
<span
|
|
@@ -9,10 +9,16 @@ export function LogoMark({ size = 28, className, spin = false }: { size?: number
|
|
|
9
9
|
aria-hidden="true"
|
|
10
10
|
>
|
|
11
11
|
<svg viewBox="0 0 32 32" width={size} height={size} fill="none" className={cn(spin && 'animate-spin-slow')}>
|
|
12
|
-
<circle cx="16" cy="16" r="
|
|
13
|
-
<
|
|
14
|
-
|
|
15
|
-
<path d="
|
|
12
|
+
<circle cx="16" cy="16" r="5.1" fill="currentColor" />
|
|
13
|
+
<circle cx="16" cy="16" r="8.4" stroke="currentColor" strokeWidth="0.9" opacity="0.22" />
|
|
14
|
+
<g stroke="currentColor" strokeWidth="2.15" strokeLinecap="round">
|
|
15
|
+
<path d="M16 4.4a11.6 11.6 0 0 1 9.7 5.5" />
|
|
16
|
+
<path d="M27.6 16a11.6 11.6 0 0 1-5.5 9.7" opacity="0.6" />
|
|
17
|
+
<path d="M16 27.6a11.6 11.6 0 0 1-9.7-5.5" opacity="0.32" />
|
|
18
|
+
<path d="M4.4 16a11.6 11.6 0 0 1 5.5-9.7" opacity="0.8" />
|
|
19
|
+
</g>
|
|
20
|
+
<g stroke="currentColor" strokeWidth="1.7" strokeLinecap="round">
|
|
21
|
+
<path d="M16 1.5v2.2M30.5 16h-2.2M16 30.5v-2.2M1.5 16h2.2" opacity="0.9" />
|
|
16
22
|
</g>
|
|
17
23
|
</svg>
|
|
18
24
|
</span>
|
|
@@ -178,6 +178,8 @@ export function ModelDeck({ open, onClose }: { open: boolean; onClose: () => voi
|
|
|
178
178
|
const [manual, setManual] = useState('')
|
|
179
179
|
const [showManual, setShowManual] = useState(false)
|
|
180
180
|
const [cursor, setCursor] = useState(0)
|
|
181
|
+
/** Only keyboard moves scroll the list; scrolling on hover fights the pointer and jitters. */
|
|
182
|
+
const keyboardCursor = useRef(true)
|
|
181
183
|
const listRef = useRef<HTMLDivElement>(null)
|
|
182
184
|
const searchRef = useRef<HTMLInputElement>(null)
|
|
183
185
|
|
|
@@ -226,11 +228,13 @@ export function ModelDeck({ open, onClose }: { open: boolean; onClose: () => voi
|
|
|
226
228
|
}, [models, query])
|
|
227
229
|
|
|
228
230
|
useLayoutEffect(() => {
|
|
231
|
+
keyboardCursor.current = true
|
|
229
232
|
setCursor(0)
|
|
230
233
|
}, [query, provider])
|
|
231
234
|
|
|
232
235
|
useEffect(() => {
|
|
233
|
-
|
|
236
|
+
if (!keyboardCursor.current) return
|
|
237
|
+
listRef.current?.querySelector<HTMLElement>(`[data-row="${cursor}"]`)?.scrollIntoView({ block: 'nearest' })
|
|
234
238
|
}, [cursor])
|
|
235
239
|
|
|
236
240
|
const choose = (model: ModelInfo) => {
|
|
@@ -258,9 +262,11 @@ export function ModelDeck({ open, onClose }: { open: boolean; onClose: () => voi
|
|
|
258
262
|
const onSearchKey = (event: KeyboardEvent<HTMLInputElement>) => {
|
|
259
263
|
if (event.key === 'ArrowDown') {
|
|
260
264
|
event.preventDefault()
|
|
265
|
+
keyboardCursor.current = true
|
|
261
266
|
setCursor((value) => Math.min(filtered.length - 1, value + 1))
|
|
262
267
|
} else if (event.key === 'ArrowUp') {
|
|
263
268
|
event.preventDefault()
|
|
269
|
+
keyboardCursor.current = true
|
|
264
270
|
setCursor((value) => Math.max(0, value - 1))
|
|
265
271
|
} else if (event.key === 'Enter') {
|
|
266
272
|
event.preventDefault()
|
|
@@ -472,7 +478,11 @@ export function ModelDeck({ open, onClose }: { open: boolean; onClose: () => voi
|
|
|
472
478
|
setCursor(i)
|
|
473
479
|
choose(model)
|
|
474
480
|
}}
|
|
475
|
-
|
|
481
|
+
onMouseEnter={() => {
|
|
482
|
+
if (highlighted) return
|
|
483
|
+
keyboardCursor.current = false
|
|
484
|
+
setCursor(i)
|
|
485
|
+
}}
|
|
476
486
|
style={{ animationDelay: `${Math.min(i * 22, 260)}ms` }}
|
|
477
487
|
className={cn(
|
|
478
488
|
'deck-row group/row relative mb-1 flex w-full items-center gap-3 overflow-hidden rounded-2xl px-3 py-2.5 text-left transition-colors',
|