@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.
Files changed (165) hide show
  1. package/.changeset/config.json +11 -0
  2. package/.env.example +2 -0
  3. package/.github/CODEOWNERS +1 -0
  4. package/.github/ISSUE_TEMPLATE/bug_report.md +16 -0
  5. package/.github/ISSUE_TEMPLATE/feature_request.md +11 -0
  6. package/.github/PULL_REQUEST_TEMPLATE.md +10 -0
  7. package/.github/dependabot.yml +11 -0
  8. package/.github/workflows/ci.yml +23 -0
  9. package/.github/workflows/release.yml +29 -0
  10. package/.node-version +1 -0
  11. package/.nvmrc +1 -0
  12. package/.prettierrc +7 -0
  13. package/.project/ACCOUNT.yaml +4 -0
  14. package/.project/IDEAS.yaml +7 -0
  15. package/.project/PROJECT.yaml +11 -0
  16. package/.project/ROADMAP.yaml +15 -0
  17. package/CHANGELOG.md +15 -0
  18. package/CODE_OF_CONDUCT.md +26 -0
  19. package/CONTRIBUTING.md +61 -0
  20. package/LICENSE +21 -0
  21. package/README.md +1 -0
  22. package/SECURITY.md +18 -0
  23. package/SUPPORT.md +14 -0
  24. package/package.json +75 -0
  25. package/packages/convex/package.json +42 -0
  26. package/packages/convex/src/index.ts +8 -0
  27. package/packages/convex/src/mutations/messages.ts +29 -0
  28. package/packages/convex/src/queries/messages.ts +24 -0
  29. package/packages/convex/src/schema.ts +20 -0
  30. package/packages/convex/tsconfig.json +11 -0
  31. package/packages/convex/tsup.config.ts +17 -0
  32. package/packages/react/README.md +1 -0
  33. package/packages/react/package.json +60 -0
  34. package/packages/react/src/components/AILogTable.tsx +90 -0
  35. package/packages/react/src/components/ChatWindow.tsx +118 -0
  36. package/packages/react/src/components/GenerationCard.tsx +73 -0
  37. package/packages/react/src/components/ImageGenerator.tsx +103 -0
  38. package/packages/react/src/components/ModelSelector.tsx +44 -0
  39. package/packages/react/src/components/ModelTestRunner.tsx +148 -0
  40. package/packages/react/src/components/VoiceSelector.tsx +51 -0
  41. package/packages/react/src/components/index.ts +9 -0
  42. package/packages/react/src/hooks/index.ts +12 -0
  43. package/packages/react/src/hooks/useAI.ts +158 -0
  44. package/packages/react/src/hooks/useAILogs.ts +40 -0
  45. package/packages/react/src/hooks/useAIModels.ts +53 -0
  46. package/packages/react/src/hooks/useChat.ts +141 -0
  47. package/packages/react/src/hooks/useContentManager.ts +108 -0
  48. package/packages/react/src/hooks/useImageGeneration.ts +82 -0
  49. package/packages/react/src/hooks/useMemory.ts +161 -0
  50. package/packages/react/src/hooks/useModelTest.ts +126 -0
  51. package/packages/react/src/hooks/useRealtimeAudio.ts +203 -0
  52. package/packages/react/src/hooks/useSkills.ts +114 -0
  53. package/packages/react/src/hooks/useTextToSpeech.ts +99 -0
  54. package/packages/react/src/hooks/useTranscription.ts +119 -0
  55. package/packages/react/src/hooks/useVideoGeneration.ts +79 -0
  56. package/packages/react/src/index.ts +42 -0
  57. package/packages/react/src/pages/AILogsPage.tsx +98 -0
  58. package/packages/react/src/pages/ChatPage.tsx +42 -0
  59. package/packages/react/src/pages/ModelTestPage.tsx +33 -0
  60. package/packages/react/src/pages/index.ts +5 -0
  61. package/packages/react/tsconfig.json +26 -0
  62. package/packages/react/tsup.config.ts +22 -0
  63. package/packages/react-css/README.md +1 -0
  64. package/packages/react-css/package.json +45 -0
  65. package/packages/react-css/src/ai.css +857 -0
  66. package/packages/react-css/src/components/AILogTable.tsx +90 -0
  67. package/packages/react-css/src/components/ChatWindow.tsx +118 -0
  68. package/packages/react-css/src/components/GenerationCard.tsx +73 -0
  69. package/packages/react-css/src/components/ImageGenerator.tsx +103 -0
  70. package/packages/react-css/src/components/ModelSelector.tsx +44 -0
  71. package/packages/react-css/src/components/ModelTestRunner.tsx +148 -0
  72. package/packages/react-css/src/components/VoiceSelector.tsx +51 -0
  73. package/packages/react-css/src/components/index.ts +9 -0
  74. package/packages/react-css/src/hooks/index.ts +12 -0
  75. package/packages/react-css/src/hooks/useAI.ts +153 -0
  76. package/packages/react-css/src/hooks/useAILogs.ts +40 -0
  77. package/packages/react-css/src/hooks/useAIModels.ts +51 -0
  78. package/packages/react-css/src/hooks/useChat.ts +145 -0
  79. package/packages/react-css/src/hooks/useContentManager.ts +108 -0
  80. package/packages/react-css/src/hooks/useImageGeneration.ts +82 -0
  81. package/packages/react-css/src/hooks/useMemory.ts +161 -0
  82. package/packages/react-css/src/hooks/useModelTest.ts +122 -0
  83. package/packages/react-css/src/hooks/useRealtimeAudio.ts +203 -0
  84. package/packages/react-css/src/hooks/useSkills.ts +114 -0
  85. package/packages/react-css/src/hooks/useTextToSpeech.ts +99 -0
  86. package/packages/react-css/src/hooks/useTranscription.ts +119 -0
  87. package/packages/react-css/src/hooks/useVideoGeneration.ts +79 -0
  88. package/packages/react-css/src/index.ts +35 -0
  89. package/packages/react-css/src/pages/AILogsPage.tsx +98 -0
  90. package/packages/react-css/src/pages/ChatPage.tsx +42 -0
  91. package/packages/react-css/src/pages/ModelTestPage.tsx +33 -0
  92. package/packages/react-css/src/pages/index.ts +5 -0
  93. package/packages/react-css/src/styles.css +127 -0
  94. package/packages/react-css/tsconfig.json +26 -0
  95. package/packages/react-css/tsup.config.ts +2 -0
  96. package/packages/shared/README.md +1 -0
  97. package/packages/shared/package.json +71 -0
  98. package/packages/shared/src/__tests__/ai.test.ts +67 -0
  99. package/packages/shared/src/ai-client.ts +243 -0
  100. package/packages/shared/src/config.ts +235 -0
  101. package/packages/shared/src/content.ts +249 -0
  102. package/packages/shared/src/convex/helpers.ts +163 -0
  103. package/packages/shared/src/convex/index.ts +16 -0
  104. package/packages/shared/src/convex/schemas.ts +146 -0
  105. package/packages/shared/src/convex/validators.ts +136 -0
  106. package/packages/shared/src/index.ts +107 -0
  107. package/packages/shared/src/memory.ts +197 -0
  108. package/packages/shared/src/providers/base.ts +103 -0
  109. package/packages/shared/src/providers/elevenlabs.ts +155 -0
  110. package/packages/shared/src/providers/index.ts +28 -0
  111. package/packages/shared/src/providers/openai-compatible.ts +286 -0
  112. package/packages/shared/src/providers/registry.ts +113 -0
  113. package/packages/shared/src/providers/replicate-fal.ts +230 -0
  114. package/packages/shared/src/skills.ts +273 -0
  115. package/packages/shared/src/types.ts +501 -0
  116. package/packages/shared/tsconfig.json +25 -0
  117. package/packages/shared/tsup.config.ts +22 -0
  118. package/packages/shared/vitest.config.ts +4 -0
  119. package/packages/solidjs/README.md +1 -0
  120. package/packages/solidjs/package.json +59 -0
  121. package/packages/solidjs/src/components/ChatWindow.tsx +78 -0
  122. package/packages/solidjs/src/components/GenerationCard.tsx +62 -0
  123. package/packages/solidjs/src/components/ModelTestRunner.tsx +119 -0
  124. package/packages/solidjs/src/components/index.ts +5 -0
  125. package/packages/solidjs/src/index.ts +32 -0
  126. package/packages/solidjs/src/pages/ChatPage.tsx +22 -0
  127. package/packages/solidjs/src/pages/ModelTestPage.tsx +22 -0
  128. package/packages/solidjs/src/pages/index.ts +4 -0
  129. package/packages/solidjs/src/primitives/createAI.ts +79 -0
  130. package/packages/solidjs/src/primitives/createChat.ts +100 -0
  131. package/packages/solidjs/src/primitives/createContentManager.ts +61 -0
  132. package/packages/solidjs/src/primitives/createImageGeneration.ts +46 -0
  133. package/packages/solidjs/src/primitives/createMemory.ts +127 -0
  134. package/packages/solidjs/src/primitives/createModelTest.ts +89 -0
  135. package/packages/solidjs/src/primitives/createSkills.ts +83 -0
  136. package/packages/solidjs/src/primitives/createTextToSpeech.ts +56 -0
  137. package/packages/solidjs/src/primitives/createVideoGeneration.ts +46 -0
  138. package/packages/solidjs/src/primitives/index.ts +8 -0
  139. package/packages/solidjs/tsconfig.json +27 -0
  140. package/packages/solidjs/tsup.config.ts +21 -0
  141. package/packages/solidjs-css/README.md +1 -0
  142. package/packages/solidjs-css/package.json +44 -0
  143. package/packages/solidjs-css/src/ai.css +857 -0
  144. package/packages/solidjs-css/src/components/ChatWindow.tsx +78 -0
  145. package/packages/solidjs-css/src/components/GenerationCard.tsx +62 -0
  146. package/packages/solidjs-css/src/components/ModelTestRunner.tsx +119 -0
  147. package/packages/solidjs-css/src/components/index.ts +5 -0
  148. package/packages/solidjs-css/src/index.ts +26 -0
  149. package/packages/solidjs-css/src/pages/ChatPage.tsx +22 -0
  150. package/packages/solidjs-css/src/pages/ModelTestPage.tsx +22 -0
  151. package/packages/solidjs-css/src/pages/index.ts +4 -0
  152. package/packages/solidjs-css/src/primitives/createAI.ts +79 -0
  153. package/packages/solidjs-css/src/primitives/createChat.ts +100 -0
  154. package/packages/solidjs-css/src/primitives/createContentManager.ts +61 -0
  155. package/packages/solidjs-css/src/primitives/createImageGeneration.ts +46 -0
  156. package/packages/solidjs-css/src/primitives/createMemory.ts +127 -0
  157. package/packages/solidjs-css/src/primitives/createModelTest.ts +89 -0
  158. package/packages/solidjs-css/src/primitives/createSkills.ts +83 -0
  159. package/packages/solidjs-css/src/primitives/createTextToSpeech.ts +56 -0
  160. package/packages/solidjs-css/src/primitives/createVideoGeneration.ts +46 -0
  161. package/packages/solidjs-css/src/primitives/index.ts +1 -0
  162. package/packages/solidjs-css/src/styles.css +127 -0
  163. package/packages/solidjs-css/tsconfig.json +27 -0
  164. package/packages/solidjs-css/tsup.config.ts +2 -0
  165. 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'