@geenius/ai 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.changeset/config.json +11 -0
- package/.env.example +2 -0
- package/.github/CODEOWNERS +1 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +16 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +11 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +10 -0
- package/.github/dependabot.yml +11 -0
- package/.github/workflows/ci.yml +23 -0
- package/.github/workflows/release.yml +29 -0
- package/.node-version +1 -0
- package/.nvmrc +1 -0
- package/.prettierrc +7 -0
- package/.project/ACCOUNT.yaml +4 -0
- package/.project/IDEAS.yaml +7 -0
- package/.project/PROJECT.yaml +11 -0
- package/.project/ROADMAP.yaml +15 -0
- package/CHANGELOG.md +15 -0
- package/CODE_OF_CONDUCT.md +26 -0
- package/CONTRIBUTING.md +61 -0
- package/LICENSE +21 -0
- package/README.md +1 -0
- package/SECURITY.md +18 -0
- package/SUPPORT.md +14 -0
- package/package.json +75 -0
- package/packages/convex/package.json +42 -0
- package/packages/convex/src/index.ts +8 -0
- package/packages/convex/src/mutations/messages.ts +29 -0
- package/packages/convex/src/queries/messages.ts +24 -0
- package/packages/convex/src/schema.ts +20 -0
- package/packages/convex/tsconfig.json +11 -0
- package/packages/convex/tsup.config.ts +17 -0
- package/packages/react/README.md +1 -0
- package/packages/react/package.json +60 -0
- package/packages/react/src/components/AILogTable.tsx +90 -0
- package/packages/react/src/components/ChatWindow.tsx +118 -0
- package/packages/react/src/components/GenerationCard.tsx +73 -0
- package/packages/react/src/components/ImageGenerator.tsx +103 -0
- package/packages/react/src/components/ModelSelector.tsx +44 -0
- package/packages/react/src/components/ModelTestRunner.tsx +148 -0
- package/packages/react/src/components/VoiceSelector.tsx +51 -0
- package/packages/react/src/components/index.ts +9 -0
- package/packages/react/src/hooks/index.ts +12 -0
- package/packages/react/src/hooks/useAI.ts +158 -0
- package/packages/react/src/hooks/useAILogs.ts +40 -0
- package/packages/react/src/hooks/useAIModels.ts +53 -0
- package/packages/react/src/hooks/useChat.ts +141 -0
- package/packages/react/src/hooks/useContentManager.ts +108 -0
- package/packages/react/src/hooks/useImageGeneration.ts +82 -0
- package/packages/react/src/hooks/useMemory.ts +161 -0
- package/packages/react/src/hooks/useModelTest.ts +126 -0
- package/packages/react/src/hooks/useRealtimeAudio.ts +203 -0
- package/packages/react/src/hooks/useSkills.ts +114 -0
- package/packages/react/src/hooks/useTextToSpeech.ts +99 -0
- package/packages/react/src/hooks/useTranscription.ts +119 -0
- package/packages/react/src/hooks/useVideoGeneration.ts +79 -0
- package/packages/react/src/index.ts +42 -0
- package/packages/react/src/pages/AILogsPage.tsx +98 -0
- package/packages/react/src/pages/ChatPage.tsx +42 -0
- package/packages/react/src/pages/ModelTestPage.tsx +33 -0
- package/packages/react/src/pages/index.ts +5 -0
- package/packages/react/tsconfig.json +26 -0
- package/packages/react/tsup.config.ts +22 -0
- package/packages/react-css/README.md +1 -0
- package/packages/react-css/package.json +45 -0
- package/packages/react-css/src/ai.css +857 -0
- package/packages/react-css/src/components/AILogTable.tsx +90 -0
- package/packages/react-css/src/components/ChatWindow.tsx +118 -0
- package/packages/react-css/src/components/GenerationCard.tsx +73 -0
- package/packages/react-css/src/components/ImageGenerator.tsx +103 -0
- package/packages/react-css/src/components/ModelSelector.tsx +44 -0
- package/packages/react-css/src/components/ModelTestRunner.tsx +148 -0
- package/packages/react-css/src/components/VoiceSelector.tsx +51 -0
- package/packages/react-css/src/components/index.ts +9 -0
- package/packages/react-css/src/hooks/index.ts +12 -0
- package/packages/react-css/src/hooks/useAI.ts +153 -0
- package/packages/react-css/src/hooks/useAILogs.ts +40 -0
- package/packages/react-css/src/hooks/useAIModels.ts +51 -0
- package/packages/react-css/src/hooks/useChat.ts +145 -0
- package/packages/react-css/src/hooks/useContentManager.ts +108 -0
- package/packages/react-css/src/hooks/useImageGeneration.ts +82 -0
- package/packages/react-css/src/hooks/useMemory.ts +161 -0
- package/packages/react-css/src/hooks/useModelTest.ts +122 -0
- package/packages/react-css/src/hooks/useRealtimeAudio.ts +203 -0
- package/packages/react-css/src/hooks/useSkills.ts +114 -0
- package/packages/react-css/src/hooks/useTextToSpeech.ts +99 -0
- package/packages/react-css/src/hooks/useTranscription.ts +119 -0
- package/packages/react-css/src/hooks/useVideoGeneration.ts +79 -0
- package/packages/react-css/src/index.ts +35 -0
- package/packages/react-css/src/pages/AILogsPage.tsx +98 -0
- package/packages/react-css/src/pages/ChatPage.tsx +42 -0
- package/packages/react-css/src/pages/ModelTestPage.tsx +33 -0
- package/packages/react-css/src/pages/index.ts +5 -0
- package/packages/react-css/src/styles.css +127 -0
- package/packages/react-css/tsconfig.json +26 -0
- package/packages/react-css/tsup.config.ts +2 -0
- package/packages/shared/README.md +1 -0
- package/packages/shared/package.json +71 -0
- package/packages/shared/src/__tests__/ai.test.ts +67 -0
- package/packages/shared/src/ai-client.ts +243 -0
- package/packages/shared/src/config.ts +235 -0
- package/packages/shared/src/content.ts +249 -0
- package/packages/shared/src/convex/helpers.ts +163 -0
- package/packages/shared/src/convex/index.ts +16 -0
- package/packages/shared/src/convex/schemas.ts +146 -0
- package/packages/shared/src/convex/validators.ts +136 -0
- package/packages/shared/src/index.ts +107 -0
- package/packages/shared/src/memory.ts +197 -0
- package/packages/shared/src/providers/base.ts +103 -0
- package/packages/shared/src/providers/elevenlabs.ts +155 -0
- package/packages/shared/src/providers/index.ts +28 -0
- package/packages/shared/src/providers/openai-compatible.ts +286 -0
- package/packages/shared/src/providers/registry.ts +113 -0
- package/packages/shared/src/providers/replicate-fal.ts +230 -0
- package/packages/shared/src/skills.ts +273 -0
- package/packages/shared/src/types.ts +501 -0
- package/packages/shared/tsconfig.json +25 -0
- package/packages/shared/tsup.config.ts +22 -0
- package/packages/shared/vitest.config.ts +4 -0
- package/packages/solidjs/README.md +1 -0
- package/packages/solidjs/package.json +59 -0
- package/packages/solidjs/src/components/ChatWindow.tsx +78 -0
- package/packages/solidjs/src/components/GenerationCard.tsx +62 -0
- package/packages/solidjs/src/components/ModelTestRunner.tsx +119 -0
- package/packages/solidjs/src/components/index.ts +5 -0
- package/packages/solidjs/src/index.ts +32 -0
- package/packages/solidjs/src/pages/ChatPage.tsx +22 -0
- package/packages/solidjs/src/pages/ModelTestPage.tsx +22 -0
- package/packages/solidjs/src/pages/index.ts +4 -0
- package/packages/solidjs/src/primitives/createAI.ts +79 -0
- package/packages/solidjs/src/primitives/createChat.ts +100 -0
- package/packages/solidjs/src/primitives/createContentManager.ts +61 -0
- package/packages/solidjs/src/primitives/createImageGeneration.ts +46 -0
- package/packages/solidjs/src/primitives/createMemory.ts +127 -0
- package/packages/solidjs/src/primitives/createModelTest.ts +89 -0
- package/packages/solidjs/src/primitives/createSkills.ts +83 -0
- package/packages/solidjs/src/primitives/createTextToSpeech.ts +56 -0
- package/packages/solidjs/src/primitives/createVideoGeneration.ts +46 -0
- package/packages/solidjs/src/primitives/index.ts +8 -0
- package/packages/solidjs/tsconfig.json +27 -0
- package/packages/solidjs/tsup.config.ts +21 -0
- package/packages/solidjs-css/README.md +1 -0
- package/packages/solidjs-css/package.json +44 -0
- package/packages/solidjs-css/src/ai.css +857 -0
- package/packages/solidjs-css/src/components/ChatWindow.tsx +78 -0
- package/packages/solidjs-css/src/components/GenerationCard.tsx +62 -0
- package/packages/solidjs-css/src/components/ModelTestRunner.tsx +119 -0
- package/packages/solidjs-css/src/components/index.ts +5 -0
- package/packages/solidjs-css/src/index.ts +26 -0
- package/packages/solidjs-css/src/pages/ChatPage.tsx +22 -0
- package/packages/solidjs-css/src/pages/ModelTestPage.tsx +22 -0
- package/packages/solidjs-css/src/pages/index.ts +4 -0
- package/packages/solidjs-css/src/primitives/createAI.ts +79 -0
- package/packages/solidjs-css/src/primitives/createChat.ts +100 -0
- package/packages/solidjs-css/src/primitives/createContentManager.ts +61 -0
- package/packages/solidjs-css/src/primitives/createImageGeneration.ts +46 -0
- package/packages/solidjs-css/src/primitives/createMemory.ts +127 -0
- package/packages/solidjs-css/src/primitives/createModelTest.ts +89 -0
- package/packages/solidjs-css/src/primitives/createSkills.ts +83 -0
- package/packages/solidjs-css/src/primitives/createTextToSpeech.ts +56 -0
- package/packages/solidjs-css/src/primitives/createVideoGeneration.ts +46 -0
- package/packages/solidjs-css/src/primitives/index.ts +1 -0
- package/packages/solidjs-css/src/styles.css +127 -0
- package/packages/solidjs-css/tsconfig.json +27 -0
- package/packages/solidjs-css/tsup.config.ts +2 -0
- package/pnpm-workspace.yaml +2 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// @geenius-ai/react — src/components/AILogTable.tsx
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* AI log table — paginated, filterable.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { AILogEntry, AILogTableProps as BaseProps } from '@geenius-ai/shared'
|
|
8
|
+
|
|
9
|
+
export interface AILogTableComponentProps extends BaseProps {
|
|
10
|
+
logs: AILogEntry[]
|
|
11
|
+
isLoading: boolean
|
|
12
|
+
onRowClick?: (log: AILogEntry) => void
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function formatDuration(ms: number): string {
|
|
16
|
+
if (ms < 1000) return `${ms}ms`
|
|
17
|
+
return `${(ms / 1000).toFixed(1)}s`
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function formatCost(usd?: number): string {
|
|
21
|
+
if (!usd) return '—'
|
|
22
|
+
if (usd < 0.01) return `$${usd.toFixed(4)}`
|
|
23
|
+
return `$${usd.toFixed(2)}`
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function StatusBadge({ status }: { status: string }) {
|
|
27
|
+
return (
|
|
28
|
+
<span data-ai-status={status}>
|
|
29
|
+
{status === 'success' ? '✓' : '✗'} {status}
|
|
30
|
+
</span>
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function AILogTable(props: AILogTableComponentProps) {
|
|
35
|
+
if (props.isLoading) {
|
|
36
|
+
return <div data-ai-component="log-table" data-ai-loading>Loading logs…</div>
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (props.logs.length === 0) {
|
|
40
|
+
return <div data-ai-component="log-table" data-ai-empty>No AI logs yet</div>
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<div className={props.className} data-ai-component="log-table">
|
|
45
|
+
<table data-ai-table>
|
|
46
|
+
<thead>
|
|
47
|
+
<tr>
|
|
48
|
+
<th>Time</th>
|
|
49
|
+
<th>Model</th>
|
|
50
|
+
<th>Provider</th>
|
|
51
|
+
<th>Caller</th>
|
|
52
|
+
<th>Status</th>
|
|
53
|
+
<th>Duration</th>
|
|
54
|
+
<th>Tokens</th>
|
|
55
|
+
<th>Cost</th>
|
|
56
|
+
</tr>
|
|
57
|
+
</thead>
|
|
58
|
+
<tbody>
|
|
59
|
+
{props.logs.map((log) => (
|
|
60
|
+
<tr
|
|
61
|
+
key={log.requestId}
|
|
62
|
+
onClick={() => props.onRowClick?.(log)}
|
|
63
|
+
data-ai-log-row
|
|
64
|
+
data-ai-status={log.status}
|
|
65
|
+
>
|
|
66
|
+
<td data-ai-cell="time">
|
|
67
|
+
{new Date(log.timestamp).toLocaleTimeString()}
|
|
68
|
+
</td>
|
|
69
|
+
<td data-ai-cell="model">{log.model}</td>
|
|
70
|
+
<td data-ai-cell="provider">{log.provider}</td>
|
|
71
|
+
<td data-ai-cell="caller">{log.caller}</td>
|
|
72
|
+
<td data-ai-cell="status">
|
|
73
|
+
<StatusBadge status={log.status} />
|
|
74
|
+
</td>
|
|
75
|
+
<td data-ai-cell="duration">
|
|
76
|
+
{formatDuration(log.durationMs)}
|
|
77
|
+
</td>
|
|
78
|
+
<td data-ai-cell="tokens">
|
|
79
|
+
{log.totalTokens ?? '—'}
|
|
80
|
+
</td>
|
|
81
|
+
<td data-ai-cell="cost">
|
|
82
|
+
{formatCost(log.totalCostUsd)}
|
|
83
|
+
</td>
|
|
84
|
+
</tr>
|
|
85
|
+
))}
|
|
86
|
+
</tbody>
|
|
87
|
+
</table>
|
|
88
|
+
</div>
|
|
89
|
+
)
|
|
90
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// @geenius-ai/react — src/components/ChatWindow.tsx
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Headless chat window: message list + input.
|
|
5
|
+
* Unstyled with data-* attributes for easy CSS targeting.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { useState, useRef, useEffect, type FormEvent } from 'react'
|
|
9
|
+
import type { AIChatMessage, ChatWindowProps } from '@geenius-ai/shared'
|
|
10
|
+
import { useChat, type UseChatOptions } from '../hooks/useChat'
|
|
11
|
+
|
|
12
|
+
export interface ChatWindowComponentProps extends ChatWindowProps, UseChatOptions {
|
|
13
|
+
renderMessage?: (message: AIChatMessage) => React.ReactNode
|
|
14
|
+
renderInput?: (props: { value: string; onChange: (v: string) => void; onSubmit: () => void; isSending: boolean }) => React.ReactNode
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function ChatWindow(props: ChatWindowComponentProps) {
|
|
18
|
+
const [input, setInput] = useState('')
|
|
19
|
+
const messagesEndRef = useRef<HTMLDivElement>(null)
|
|
20
|
+
const chat = useChat(props)
|
|
21
|
+
|
|
22
|
+
// Auto-scroll to bottom
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
|
|
25
|
+
}, [chat.messages.length])
|
|
26
|
+
|
|
27
|
+
const handleSubmit = async (e?: FormEvent) => {
|
|
28
|
+
e?.preventDefault()
|
|
29
|
+
if (!input.trim() || chat.isSending) return
|
|
30
|
+
const content = input.trim()
|
|
31
|
+
setInput('')
|
|
32
|
+
await chat.sendMessage(content)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<div className={props.className} data-ai-component="chat-window">
|
|
37
|
+
{/* Messages */}
|
|
38
|
+
<div data-ai-messages>
|
|
39
|
+
{chat.messages.length === 0 && (
|
|
40
|
+
<div data-ai-empty>
|
|
41
|
+
<p>Start a conversation</p>
|
|
42
|
+
</div>
|
|
43
|
+
)}
|
|
44
|
+
|
|
45
|
+
{chat.messages.map((msg: AIChatMessage) =>
|
|
46
|
+
props.renderMessage ? (
|
|
47
|
+
props.renderMessage(msg)
|
|
48
|
+
) : (
|
|
49
|
+
<div
|
|
50
|
+
key={msg.id}
|
|
51
|
+
data-ai-message
|
|
52
|
+
data-ai-role={msg.role}
|
|
53
|
+
>
|
|
54
|
+
<div data-ai-message-role>{msg.role}</div>
|
|
55
|
+
<div data-ai-message-content>{msg.content}</div>
|
|
56
|
+
{msg.tokens && (
|
|
57
|
+
<span data-ai-message-tokens>
|
|
58
|
+
{msg.tokens} tokens
|
|
59
|
+
</span>
|
|
60
|
+
)}
|
|
61
|
+
</div>
|
|
62
|
+
)
|
|
63
|
+
)}
|
|
64
|
+
|
|
65
|
+
{chat.isSending && (
|
|
66
|
+
<div data-ai-message data-ai-role="assistant" data-ai-loading>
|
|
67
|
+
<div data-ai-typing-indicator>
|
|
68
|
+
<span /><span /><span />
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
)}
|
|
72
|
+
|
|
73
|
+
<div ref={messagesEndRef} />
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
{/* Error */}
|
|
77
|
+
{chat.error && (
|
|
78
|
+
<div data-ai-error role="alert">
|
|
79
|
+
<span>{chat.error}</span>
|
|
80
|
+
<button onClick={chat.clearError} data-ai-dismiss>×</button>
|
|
81
|
+
</div>
|
|
82
|
+
)}
|
|
83
|
+
|
|
84
|
+
{/* Input */}
|
|
85
|
+
{props.renderInput ? (
|
|
86
|
+
props.renderInput({
|
|
87
|
+
value: input,
|
|
88
|
+
onChange: setInput,
|
|
89
|
+
onSubmit: handleSubmit,
|
|
90
|
+
isSending: chat.isSending,
|
|
91
|
+
})
|
|
92
|
+
) : (
|
|
93
|
+
<form onSubmit={handleSubmit} data-ai-input-form>
|
|
94
|
+
<textarea
|
|
95
|
+
value={input}
|
|
96
|
+
onChange={(e) => setInput(e.target.value)}
|
|
97
|
+
placeholder="Type a message…"
|
|
98
|
+
disabled={chat.isSending}
|
|
99
|
+
data-ai-input
|
|
100
|
+
onKeyDown={(e) => {
|
|
101
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
102
|
+
e.preventDefault()
|
|
103
|
+
handleSubmit()
|
|
104
|
+
}
|
|
105
|
+
}}
|
|
106
|
+
/>
|
|
107
|
+
<button
|
|
108
|
+
type="submit"
|
|
109
|
+
disabled={chat.isSending || !input.trim()}
|
|
110
|
+
data-ai-send
|
|
111
|
+
>
|
|
112
|
+
{chat.isSending ? 'Sending…' : 'Send'}
|
|
113
|
+
</button>
|
|
114
|
+
</form>
|
|
115
|
+
)}
|
|
116
|
+
</div>
|
|
117
|
+
)
|
|
118
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// @geenius-ai/react — src/components/GenerationCard.tsx
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Card displaying a generation result (text, image, audio, video).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { AIGenerationType } from '@geenius-ai/shared'
|
|
8
|
+
|
|
9
|
+
export interface GenerationCardProps {
|
|
10
|
+
type: AIGenerationType
|
|
11
|
+
content: string
|
|
12
|
+
model?: string
|
|
13
|
+
durationMs?: number
|
|
14
|
+
tokens?: number
|
|
15
|
+
cost?: number
|
|
16
|
+
error?: string
|
|
17
|
+
className?: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function GenerationCard(props: GenerationCardProps) {
|
|
21
|
+
return (
|
|
22
|
+
<div className={props.className} data-ai-component="generation-card" data-ai-type={props.type}>
|
|
23
|
+
{/* Header */}
|
|
24
|
+
<div data-ai-card-header>
|
|
25
|
+
<span data-ai-card-type>{props.type}</span>
|
|
26
|
+
{props.model && <span data-ai-card-model>{props.model}</span>}
|
|
27
|
+
{props.durationMs != null && (
|
|
28
|
+
<span data-ai-card-duration>{props.durationMs}ms</span>
|
|
29
|
+
)}
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
{/* Content */}
|
|
33
|
+
{props.error ? (
|
|
34
|
+
<div data-ai-card-error role="alert">{props.error}</div>
|
|
35
|
+
) : (
|
|
36
|
+
<div data-ai-card-content>
|
|
37
|
+
{props.type === 'image' ? (
|
|
38
|
+
<img
|
|
39
|
+
src={props.content.startsWith('http') ? props.content : `data:image/png;base64,${props.content}`}
|
|
40
|
+
alt="AI Generated"
|
|
41
|
+
data-ai-card-image
|
|
42
|
+
/>
|
|
43
|
+
) : props.type === 'audio' || props.type === 'speech' ? (
|
|
44
|
+
<audio controls data-ai-card-audio>
|
|
45
|
+
<source
|
|
46
|
+
src={props.content.startsWith('http') ? props.content : `data:audio/mp3;base64,${props.content}`}
|
|
47
|
+
type="audio/mp3"
|
|
48
|
+
/>
|
|
49
|
+
</audio>
|
|
50
|
+
) : props.type === 'video' ? (
|
|
51
|
+
<video controls data-ai-card-video>
|
|
52
|
+
<source src={props.content} type="video/mp4" />
|
|
53
|
+
</video>
|
|
54
|
+
) : (
|
|
55
|
+
<pre data-ai-card-text>{props.content}</pre>
|
|
56
|
+
)}
|
|
57
|
+
</div>
|
|
58
|
+
)}
|
|
59
|
+
|
|
60
|
+
{/* Footer */}
|
|
61
|
+
{(props.tokens || props.cost != null) && (
|
|
62
|
+
<div data-ai-card-footer>
|
|
63
|
+
{props.tokens && <span data-ai-card-tokens>{props.tokens} tokens</span>}
|
|
64
|
+
{props.cost != null && (
|
|
65
|
+
<span data-ai-card-cost>
|
|
66
|
+
${props.cost < 0.01 ? props.cost.toFixed(4) : props.cost.toFixed(2)}
|
|
67
|
+
</span>
|
|
68
|
+
)}
|
|
69
|
+
</div>
|
|
70
|
+
)}
|
|
71
|
+
</div>
|
|
72
|
+
)
|
|
73
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// @geenius-ai/react — src/components/ImageGenerator.tsx
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Image generation UI — prompt, model selector, gallery.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { useState, type FormEvent } from 'react'
|
|
8
|
+
import type { AIGenerationType } from '@geenius-ai/shared'
|
|
9
|
+
import { useImageGeneration, type UseImageGenerationOptions } from '../hooks/useImageGeneration'
|
|
10
|
+
|
|
11
|
+
export interface ImageGeneratorComponentProps extends UseImageGenerationOptions {
|
|
12
|
+
className?: string
|
|
13
|
+
availableModels?: string[]
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function ImageGenerator(props: ImageGeneratorComponentProps) {
|
|
17
|
+
const [prompt, setPrompt] = useState('')
|
|
18
|
+
const [selectedModel, setSelectedModel] = useState(props.defaultModel ?? '')
|
|
19
|
+
const [size, setSize] = useState('1024x1024')
|
|
20
|
+
const [quality, setQuality] = useState<'standard' | 'hd'>('standard')
|
|
21
|
+
|
|
22
|
+
const { generate, images, isGenerating, error, clearImages, clearError } = useImageGeneration(props)
|
|
23
|
+
|
|
24
|
+
const handleSubmit = async (e: FormEvent) => {
|
|
25
|
+
e.preventDefault()
|
|
26
|
+
if (!prompt.trim() || isGenerating) return
|
|
27
|
+
await generate(prompt.trim(), { model: selectedModel || undefined, size, quality })
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<div className={props.className} data-ai-component="image-generator">
|
|
32
|
+
<form onSubmit={handleSubmit} data-ai-image-form>
|
|
33
|
+
<div data-ai-field="prompt">
|
|
34
|
+
<textarea
|
|
35
|
+
value={prompt}
|
|
36
|
+
onChange={(e) => setPrompt(e.target.value)}
|
|
37
|
+
placeholder="Describe the image you want to create…"
|
|
38
|
+
disabled={isGenerating}
|
|
39
|
+
data-ai-input
|
|
40
|
+
/>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
<div data-ai-image-options>
|
|
44
|
+
{props.availableModels && (
|
|
45
|
+
<select
|
|
46
|
+
value={selectedModel}
|
|
47
|
+
onChange={(e) => setSelectedModel(e.target.value)}
|
|
48
|
+
disabled={isGenerating}
|
|
49
|
+
data-ai-select
|
|
50
|
+
>
|
|
51
|
+
<option value="">Default model</option>
|
|
52
|
+
{props.availableModels.map(m => (
|
|
53
|
+
<option key={m} value={m}>{m}</option>
|
|
54
|
+
))}
|
|
55
|
+
</select>
|
|
56
|
+
)}
|
|
57
|
+
|
|
58
|
+
<select value={size} onChange={(e) => setSize(e.target.value)} disabled={isGenerating} data-ai-select>
|
|
59
|
+
<option value="1024x1024">1024×1024</option>
|
|
60
|
+
<option value="1792x1024">1792×1024</option>
|
|
61
|
+
<option value="1024x1792">1024×1792</option>
|
|
62
|
+
<option value="512x512">512×512</option>
|
|
63
|
+
</select>
|
|
64
|
+
|
|
65
|
+
<select value={quality} onChange={(e) => setQuality(e.target.value as 'standard' | 'hd')} disabled={isGenerating} data-ai-select>
|
|
66
|
+
<option value="standard">Standard</option>
|
|
67
|
+
<option value="hd">HD</option>
|
|
68
|
+
</select>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
<div data-ai-actions>
|
|
72
|
+
<button type="submit" disabled={isGenerating || !prompt.trim()} data-ai-submit>
|
|
73
|
+
{isGenerating ? 'Generating…' : 'Generate Image'}
|
|
74
|
+
</button>
|
|
75
|
+
{images.length > 0 && (
|
|
76
|
+
<button type="button" onClick={clearImages} data-ai-clear>Clear Gallery</button>
|
|
77
|
+
)}
|
|
78
|
+
</div>
|
|
79
|
+
</form>
|
|
80
|
+
|
|
81
|
+
{error && (
|
|
82
|
+
<div data-ai-error role="alert">
|
|
83
|
+
<span>{error}</span>
|
|
84
|
+
<button onClick={clearError}>×</button>
|
|
85
|
+
</div>
|
|
86
|
+
)}
|
|
87
|
+
|
|
88
|
+
{images.length > 0 && (
|
|
89
|
+
<div data-ai-image-gallery>
|
|
90
|
+
{images.map((img, i) => (
|
|
91
|
+
<div key={i} data-ai-image-card>
|
|
92
|
+
<img src={img.url} alt={img.prompt} data-ai-generated-image />
|
|
93
|
+
<div data-ai-image-meta>
|
|
94
|
+
<span data-ai-image-model>{img.model}</span>
|
|
95
|
+
<span data-ai-image-time>{new Date(img.timestamp).toLocaleTimeString()}</span>
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
))}
|
|
99
|
+
</div>
|
|
100
|
+
)}
|
|
101
|
+
</div>
|
|
102
|
+
)
|
|
103
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// @geenius-ai/react — src/components/ModelSelector.tsx
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Model selector dropdown / list.
|
|
5
|
+
* Unstyled with data-* attributes.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { AIModel, AIProviderType } from '@geenius-ai/shared'
|
|
9
|
+
|
|
10
|
+
export interface ModelSelectorProps {
|
|
11
|
+
models: AIModel[]
|
|
12
|
+
selectedModel?: string
|
|
13
|
+
onSelect: (modelId: string) => void
|
|
14
|
+
className?: string
|
|
15
|
+
filterProvider?: AIProviderType
|
|
16
|
+
showCost?: boolean
|
|
17
|
+
disabled?: boolean
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function ModelSelector(props: ModelSelectorProps) {
|
|
21
|
+
const filtered = props.filterProvider
|
|
22
|
+
? props.models.filter(m => m.provider === props.filterProvider)
|
|
23
|
+
: props.models
|
|
24
|
+
|
|
25
|
+
const activeModels = filtered.filter(m => m.isActive)
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<select
|
|
29
|
+
className={props.className}
|
|
30
|
+
value={props.selectedModel ?? ''}
|
|
31
|
+
onChange={(e) => props.onSelect(e.target.value)}
|
|
32
|
+
disabled={props.disabled}
|
|
33
|
+
data-ai-component="model-selector"
|
|
34
|
+
>
|
|
35
|
+
<option value="" disabled>Select a model…</option>
|
|
36
|
+
{activeModels.map(model => (
|
|
37
|
+
<option key={model.id} value={model.id} data-ai-provider={model.provider}>
|
|
38
|
+
{model.displayName ?? model.name}
|
|
39
|
+
{props.showCost ? ` ($${model.inputCostPer1k}/$${model.outputCostPer1k}/1k)` : ''}
|
|
40
|
+
</option>
|
|
41
|
+
))}
|
|
42
|
+
</select>
|
|
43
|
+
)
|
|
44
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// @geenius-ai/react — src/components/ModelTestRunner.tsx
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Multi-tab model test runner.
|
|
5
|
+
* Extracted from ModelTestPage.tsx pattern found in 15+ apps.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { useState, type FormEvent } from 'react'
|
|
9
|
+
import type { AIGenerationType } from '@geenius-ai/shared'
|
|
10
|
+
import { useModelTest, type UseModelTestOptions, type ModelTestResult } from '../hooks/useModelTest'
|
|
11
|
+
|
|
12
|
+
export interface ModelTestRunnerProps extends UseModelTestOptions {
|
|
13
|
+
className?: string
|
|
14
|
+
availableModels?: string[]
|
|
15
|
+
defaultTab?: AIGenerationType
|
|
16
|
+
renderResult?: (result: ModelTestResult) => React.ReactNode
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const TABS: { id: AIGenerationType; label: string }[] = [
|
|
20
|
+
{ id: 'text', label: 'Text Generation' },
|
|
21
|
+
{ id: 'image', label: 'Image Generation' },
|
|
22
|
+
{ id: 'audio', label: 'Text-to-Speech' },
|
|
23
|
+
{ id: 'transcription', label: 'Audio-to-Text' },
|
|
24
|
+
{ id: 'video', label: 'Video Generation' },
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
export function ModelTestRunner(props: ModelTestRunnerProps) {
|
|
28
|
+
const [activeTab, setActiveTab] = useState<AIGenerationType>(props.defaultTab ?? 'text')
|
|
29
|
+
const [prompt, setPrompt] = useState('')
|
|
30
|
+
const [selectedModel, setSelectedModel] = useState('')
|
|
31
|
+
const [batchMode, setBatchMode] = useState(false)
|
|
32
|
+
|
|
33
|
+
const test = useModelTest(props)
|
|
34
|
+
|
|
35
|
+
const handleSubmit = async (e: FormEvent) => {
|
|
36
|
+
e.preventDefault()
|
|
37
|
+
if (!prompt.trim()) return
|
|
38
|
+
|
|
39
|
+
if (batchMode && props.availableModels) {
|
|
40
|
+
await test.runBatchTest(props.availableModels, prompt.trim())
|
|
41
|
+
} else if (selectedModel) {
|
|
42
|
+
await test.runTest(selectedModel, prompt.trim(), activeTab)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<div className={props.className} data-ai-component="model-test-runner">
|
|
48
|
+
{/* Tabs */}
|
|
49
|
+
<div data-ai-tabs role="tablist">
|
|
50
|
+
{TABS.map(tab => (
|
|
51
|
+
<button
|
|
52
|
+
key={tab.id}
|
|
53
|
+
role="tab"
|
|
54
|
+
aria-selected={activeTab === tab.id}
|
|
55
|
+
onClick={() => setActiveTab(tab.id)}
|
|
56
|
+
data-ai-tab
|
|
57
|
+
data-ai-tab-active={activeTab === tab.id ? '' : undefined}
|
|
58
|
+
>
|
|
59
|
+
{tab.label}
|
|
60
|
+
</button>
|
|
61
|
+
))}
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
{/* Form */}
|
|
65
|
+
<form onSubmit={handleSubmit} data-ai-test-form>
|
|
66
|
+
<div data-ai-field="model">
|
|
67
|
+
<label htmlFor="test-model">Model</label>
|
|
68
|
+
<select
|
|
69
|
+
id="test-model"
|
|
70
|
+
value={selectedModel}
|
|
71
|
+
onChange={(e) => setSelectedModel(e.target.value)}
|
|
72
|
+
disabled={test.isRunning || batchMode}
|
|
73
|
+
data-ai-input
|
|
74
|
+
>
|
|
75
|
+
<option value="">Select model…</option>
|
|
76
|
+
{(props.availableModels ?? []).map(m => (
|
|
77
|
+
<option key={m} value={m}>{m}</option>
|
|
78
|
+
))}
|
|
79
|
+
</select>
|
|
80
|
+
</div>
|
|
81
|
+
|
|
82
|
+
<div data-ai-field="prompt">
|
|
83
|
+
<label htmlFor="test-prompt">Prompt</label>
|
|
84
|
+
<textarea
|
|
85
|
+
id="test-prompt"
|
|
86
|
+
value={prompt}
|
|
87
|
+
onChange={(e) => setPrompt(e.target.value)}
|
|
88
|
+
placeholder="Enter your test prompt…"
|
|
89
|
+
disabled={test.isRunning}
|
|
90
|
+
data-ai-input
|
|
91
|
+
/>
|
|
92
|
+
</div>
|
|
93
|
+
|
|
94
|
+
<div data-ai-actions>
|
|
95
|
+
<label data-ai-toggle>
|
|
96
|
+
<input
|
|
97
|
+
type="checkbox"
|
|
98
|
+
checked={batchMode}
|
|
99
|
+
onChange={(e) => setBatchMode(e.target.checked)}
|
|
100
|
+
disabled={test.isRunning}
|
|
101
|
+
/>
|
|
102
|
+
Test all models
|
|
103
|
+
</label>
|
|
104
|
+
<button type="submit" disabled={test.isRunning || !prompt.trim()} data-ai-submit>
|
|
105
|
+
{test.isRunning ? 'Running…' : (batchMode ? 'Run All' : 'Run Test')}
|
|
106
|
+
</button>
|
|
107
|
+
{test.results.length > 0 && (
|
|
108
|
+
<button type="button" onClick={test.clearResults} data-ai-clear>
|
|
109
|
+
Clear Results
|
|
110
|
+
</button>
|
|
111
|
+
)}
|
|
112
|
+
</div>
|
|
113
|
+
</form>
|
|
114
|
+
|
|
115
|
+
{/* Results */}
|
|
116
|
+
{test.results.length > 0 && (
|
|
117
|
+
<div data-ai-test-results>
|
|
118
|
+
{test.results.map((result, i) => (
|
|
119
|
+
props.renderResult ? props.renderResult(result) : (
|
|
120
|
+
<div key={i} data-ai-test-result data-ai-status={result.error ? 'error' : 'success'}>
|
|
121
|
+
<div data-ai-result-header>
|
|
122
|
+
<span data-ai-result-model>{result.model}</span>
|
|
123
|
+
<span data-ai-result-duration>{result.durationMs}ms</span>
|
|
124
|
+
{result.error && (
|
|
125
|
+
<span data-ai-result-error>{result.error}</span>
|
|
126
|
+
)}
|
|
127
|
+
</div>
|
|
128
|
+
{!result.error && (
|
|
129
|
+
<div data-ai-result-content>
|
|
130
|
+
{result.type === 'image' ? (
|
|
131
|
+
<img
|
|
132
|
+
src={result.result.startsWith('http') ? result.result : `data:image/png;base64,${result.result}`}
|
|
133
|
+
alt="Generated"
|
|
134
|
+
data-ai-result-image
|
|
135
|
+
/>
|
|
136
|
+
) : (
|
|
137
|
+
<pre data-ai-result-text>{result.result}</pre>
|
|
138
|
+
)}
|
|
139
|
+
</div>
|
|
140
|
+
)}
|
|
141
|
+
</div>
|
|
142
|
+
)
|
|
143
|
+
))}
|
|
144
|
+
</div>
|
|
145
|
+
)}
|
|
146
|
+
</div>
|
|
147
|
+
)
|
|
148
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// @geenius-ai/react — src/components/VoiceSelector.tsx
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Voice selector for TTS — works with OpenAI and ElevenLabs voices.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { AIVoiceOption } from '@geenius-ai/shared'
|
|
8
|
+
|
|
9
|
+
export interface VoiceSelectorComponentProps {
|
|
10
|
+
voices: AIVoiceOption[]
|
|
11
|
+
selectedVoice?: string
|
|
12
|
+
onSelect: (voiceId: string) => void
|
|
13
|
+
className?: string
|
|
14
|
+
disabled?: boolean
|
|
15
|
+
showProvider?: boolean
|
|
16
|
+
showPreview?: boolean
|
|
17
|
+
onPreview?: (voiceId: string) => void
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function VoiceSelector(props: VoiceSelectorComponentProps) {
|
|
21
|
+
return (
|
|
22
|
+
<div className={props.className} data-ai-component="voice-selector">
|
|
23
|
+
{props.voices.map(voice => (
|
|
24
|
+
<button
|
|
25
|
+
key={voice.id}
|
|
26
|
+
onClick={() => props.onSelect(voice.id)}
|
|
27
|
+
disabled={props.disabled}
|
|
28
|
+
data-ai-voice
|
|
29
|
+
data-ai-voice-selected={props.selectedVoice === voice.id ? '' : undefined}
|
|
30
|
+
data-ai-provider={voice.provider}
|
|
31
|
+
>
|
|
32
|
+
<span data-ai-voice-name>{voice.name}</span>
|
|
33
|
+
{voice.gender && <span data-ai-voice-gender>{voice.gender}</span>}
|
|
34
|
+
{voice.language && <span data-ai-voice-language>{voice.language}</span>}
|
|
35
|
+
{props.showProvider && <span data-ai-voice-provider>{voice.provider}</span>}
|
|
36
|
+
{props.showPreview && voice.preview && (
|
|
37
|
+
<button
|
|
38
|
+
data-ai-voice-preview
|
|
39
|
+
onClick={(e) => {
|
|
40
|
+
e.stopPropagation()
|
|
41
|
+
props.onPreview?.(voice.id)
|
|
42
|
+
}}
|
|
43
|
+
>
|
|
44
|
+
▶
|
|
45
|
+
</button>
|
|
46
|
+
)}
|
|
47
|
+
</button>
|
|
48
|
+
))}
|
|
49
|
+
</div>
|
|
50
|
+
)
|
|
51
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// @geenius-ai/react — src/components/index.ts
|
|
2
|
+
|
|
3
|
+
export { ChatWindow, type ChatWindowComponentProps } from './ChatWindow'
|
|
4
|
+
export { ModelSelector, type ModelSelectorProps } from './ModelSelector'
|
|
5
|
+
export { AILogTable, type AILogTableComponentProps } from './AILogTable'
|
|
6
|
+
export { ModelTestRunner, type ModelTestRunnerProps } from './ModelTestRunner'
|
|
7
|
+
export { GenerationCard, type GenerationCardProps } from './GenerationCard'
|
|
8
|
+
export { ImageGenerator, type ImageGeneratorComponentProps } from './ImageGenerator'
|
|
9
|
+
export { VoiceSelector, type VoiceSelectorComponentProps } from './VoiceSelector'
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// @geenius-ai/react — src/hooks/index.ts
|
|
2
|
+
|
|
3
|
+
export { useAI, type UseAIOptions, type UseAIReturn } from './useAI'
|
|
4
|
+
export { useChat, type UseChatOptions, type UseChatReturn } from './useChat'
|
|
5
|
+
export { useAILogs, type UseAILogsOptions, type UseAILogsReturn } from './useAILogs'
|
|
6
|
+
export { useModelTest, type UseModelTestOptions, type UseModelTestReturn, type ModelTestResult } from './useModelTest'
|
|
7
|
+
export { useAIModels, type UseAIModelsOptions, type UseAIModelsReturn } from './useAIModels'
|
|
8
|
+
export { useImageGeneration, type UseImageGenerationOptions, type UseImageGenerationReturn, type GeneratedImage } from './useImageGeneration'
|
|
9
|
+
export { useTextToSpeech, type UseTextToSpeechOptions, type UseTextToSpeechReturn } from './useTextToSpeech'
|
|
10
|
+
export { useVideoGeneration, type UseVideoGenerationOptions, type UseVideoGenerationReturn, type GeneratedVideo } from './useVideoGeneration'
|
|
11
|
+
export { useTranscription, type UseTranscriptionOptions, type UseTranscriptionReturn, type TranscriptionResult } from './useTranscription'
|
|
12
|
+
export { useRealtimeAudio, type UseRealtimeAudioOptions, type UseRealtimeAudioReturn } from './useRealtimeAudio'
|