Shih-hungg commited on
Commit
34d6b36
·
1 Parent(s): 7a0df5b

Update article generation and use langfuse

Browse files
package-lock.json CHANGED
@@ -11,6 +11,7 @@
11
  "@ai-sdk/openai": "^2.0.0",
12
  "@ai-sdk/react": "^2.0.1",
13
  "@hookform/resolvers": "^5.2.1",
 
14
  "@radix-ui/react-checkbox": "^1.3.3",
15
  "@radix-ui/react-dialog": "^1.1.15",
16
  "@radix-ui/react-label": "^2.1.7",
@@ -659,6 +660,23 @@
659
  "@jridgewell/sourcemap-codec": "^1.4.14"
660
  }
661
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
662
  "node_modules/@napi-rs/wasm-runtime": {
663
  "version": "0.2.12",
664
  "resolved": "http://artifactory-build.taboolasyndication.com:80/artifactory/api/npm/npm-public/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz",
@@ -4570,6 +4588,14 @@
4570
  "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
4571
  "dev": true
4572
  },
 
 
 
 
 
 
 
 
4573
  "node_modules/nanoid": {
4574
  "version": "3.3.11",
4575
  "resolved": "http://artifactory-build.taboolasyndication.com:80/artifactory/api/npm/npm-public/nanoid/-/nanoid-3.3.11.tgz",
 
11
  "@ai-sdk/openai": "^2.0.0",
12
  "@ai-sdk/react": "^2.0.1",
13
  "@hookform/resolvers": "^5.2.1",
14
+ "@langfuse/client": "^4.0.1",
15
  "@radix-ui/react-checkbox": "^1.3.3",
16
  "@radix-ui/react-dialog": "^1.1.15",
17
  "@radix-ui/react-label": "^2.1.7",
 
660
  "@jridgewell/sourcemap-codec": "^1.4.14"
661
  }
662
  },
663
+ "node_modules/@langfuse/client": {
664
+ "version": "4.0.1",
665
+ "resolved": "http://artifactory-build.taboolasyndication.com:80/artifactory/api/npm/npm-public/@langfuse/client/-/client-4.0.1.tgz",
666
+ "integrity": "sha512-aeY8VX8zR4DyNQ58sMyWW5xI3KqJ4zfi/TReJORTLkxx8WTSuEnyiFdj6gXDO9bzBXRexJkq2l9tCAldUyp4xQ==",
667
+ "dependencies": {
668
+ "@langfuse/core": "^4.0.1",
669
+ "mustache": "^4.2.0"
670
+ },
671
+ "peerDependencies": {
672
+ "@opentelemetry/api": "^1.9.0"
673
+ }
674
+ },
675
+ "node_modules/@langfuse/core": {
676
+ "version": "4.0.1",
677
+ "resolved": "http://artifactory-build.taboolasyndication.com:80/artifactory/api/npm/npm-public/@langfuse/core/-/core-4.0.1.tgz",
678
+ "integrity": "sha512-+KfFvFzt1yFrFH+U18Q5GMNihGbxt/ApJhHD0n/RsffTb23yFUZz1Ya7Sl/3YRZzSmWDuTWHePAHSTRrCylzTQ=="
679
+ },
680
  "node_modules/@napi-rs/wasm-runtime": {
681
  "version": "0.2.12",
682
  "resolved": "http://artifactory-build.taboolasyndication.com:80/artifactory/api/npm/npm-public/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz",
 
4588
  "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
4589
  "dev": true
4590
  },
4591
+ "node_modules/mustache": {
4592
+ "version": "4.2.0",
4593
+ "resolved": "http://artifactory-build.taboolasyndication.com:80/artifactory/api/npm/npm-public/mustache/-/mustache-4.2.0.tgz",
4594
+ "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==",
4595
+ "bin": {
4596
+ "mustache": "bin/mustache"
4597
+ }
4598
+ },
4599
  "node_modules/nanoid": {
4600
  "version": "3.3.11",
4601
  "resolved": "http://artifactory-build.taboolasyndication.com:80/artifactory/api/npm/npm-public/nanoid/-/nanoid-3.3.11.tgz",
package.json CHANGED
@@ -12,6 +12,7 @@
12
  "@ai-sdk/openai": "^2.0.0",
13
  "@ai-sdk/react": "^2.0.1",
14
  "@hookform/resolvers": "^5.2.1",
 
15
  "@radix-ui/react-checkbox": "^1.3.3",
16
  "@radix-ui/react-dialog": "^1.1.15",
17
  "@radix-ui/react-label": "^2.1.7",
 
12
  "@ai-sdk/openai": "^2.0.0",
13
  "@ai-sdk/react": "^2.0.1",
14
  "@hookform/resolvers": "^5.2.1",
15
+ "@langfuse/client": "^4.0.1",
16
  "@radix-ui/react-checkbox": "^1.3.3",
17
  "@radix-ui/react-dialog": "^1.1.15",
18
  "@radix-ui/react-label": "^2.1.7",
src/app/api/generate-article/route.ts CHANGED
@@ -1,6 +1,7 @@
1
  import { openai } from '@ai-sdk/openai';
2
  import { generateText } from 'ai';
3
- import { generateQuizPrompt } from '@/prompts/prompt-management';
 
4
 
5
  export async function POST(req: Request) {
6
  try {
@@ -14,19 +15,20 @@ export async function POST(req: Request) {
14
  inputArticle
15
  } = await req.json();
16
 
17
- // Generate the article using the article-generation prompt
18
- const prompt = generateQuizPrompt({
19
- gradeValues: grade.join(', '),
20
- unitValues: unit.join(', '),
21
- textbookVocabValues: textbookVocab,
22
- additionalVocabValues: additionalVocab,
23
- grammarValues: grammar.join(', '),
24
- topicValues: topic.join(', '),
25
- inputArticleValue: inputArticle
26
- }, 'article-generation');
 
27
 
28
  const result = await generateText({
29
- model: openai('gpt-4o-mini'),
30
  prompt: prompt,
31
  });
32
 
 
1
  import { openai } from '@ai-sdk/openai';
2
  import { generateText } from 'ai';
3
+ import { langfuse } from '@/lib/langfuse';
4
+ import { AI_CONFIG } from '@/config/ai';
5
 
6
  export async function POST(req: Request) {
7
  try {
 
15
  inputArticle
16
  } = await req.json();
17
 
18
+ // Get the prompt from Langfuse (FREE - no cost for prompt management)
19
+ const langfusePrompt = await langfuse.prompt.get('article/generation');
20
+
21
+ // Replace placeholders with actual values
22
+ const prompt = langfusePrompt.prompt
23
+ .replace(/\{\{grade_values\}\}/g, grade.join(', '))
24
+ .replace(/\{\{unit_values\}\}/g, unit.join(', '))
25
+ .replace(/\{\{textbook_vocab_values\}\}/g, textbookVocab)
26
+ .replace(/\{\{additional_vocab_values\}\}/g, additionalVocab)
27
+ .replace(/\{\{grammar_values\}\}/g, grammar.join(', '))
28
+ .replace(/\{\{topic_values\}\}/g, topic.join(', '));
29
 
30
  const result = await generateText({
31
+ model: openai(AI_CONFIG.model),
32
  prompt: prompt,
33
  });
34
 
src/app/page.tsx CHANGED
@@ -4,7 +4,7 @@ import { useState } from 'react';
4
  import { useChat } from '@ai-sdk/react';
5
  import { QuestionParameterForm } from '@/components/question-creation';
6
  import { QuestionList } from '@/components/question-output';
7
- import { SourceArticle } from '@/components/source-article';
8
  import { QuestionType, QuestionParameters, GeneratedQuestion } from '@/types/quiz';
9
  import { Button } from '@/components/ui/button';
10
  import { Input } from '@/components/ui/input';
@@ -257,7 +257,7 @@ export default function QuestionBuilder() {
257
  {/* Right side: Two-panel stack */}
258
  <div className="flex-1 flex flex-col">
259
  {/* Upper Right Panel: Source Article Editor */}
260
- <SourceArticle
261
  className="h-1/2"
262
  sourceArticle={sourceArticle}
263
  onSourceArticleChange={setSourceArticle}
 
4
  import { useChat } from '@ai-sdk/react';
5
  import { QuestionParameterForm } from '@/components/question-creation';
6
  import { QuestionList } from '@/components/question-output';
7
+ import { Article } from '@/components/article';
8
  import { QuestionType, QuestionParameters, GeneratedQuestion } from '@/types/quiz';
9
  import { Button } from '@/components/ui/button';
10
  import { Input } from '@/components/ui/input';
 
257
  {/* Right side: Two-panel stack */}
258
  <div className="flex-1 flex flex-col">
259
  {/* Upper Right Panel: Source Article Editor */}
260
+ <Article
261
  className="h-1/2"
262
  sourceArticle={sourceArticle}
263
  onSourceArticleChange={setSourceArticle}
src/components/{source-article/SourceArticle.tsx → article/Article.tsx} RENAMED
@@ -8,7 +8,7 @@ import { Label } from '@/components/ui/label';
8
  import { Textarea } from '@/components/ui/textarea';
9
  import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
10
 
11
- interface SourceArticleProps {
12
  className?: string;
13
  sourceArticle: string;
14
  onSourceArticleChange: (text: string) => void;
@@ -16,13 +16,14 @@ interface SourceArticleProps {
16
  onSourceLockedChange: (locked: boolean) => void;
17
  }
18
 
19
- export function SourceArticle({
20
  className = '',
21
  sourceArticle,
22
  onSourceArticleChange,
23
  isSourceLocked,
24
  onSourceLockedChange,
25
- }: SourceArticleProps) {
 
26
  const [wordCount, setWordCount] = useState(0);
27
  const [isModalOpen, setIsModalOpen] = useState(false);
28
  const [isGeneratingArticle, setIsGeneratingArticle] = useState(false);
@@ -51,7 +52,7 @@ export function SourceArticle({
51
  headers: { 'Content-Type': 'application/json' },
52
  body: JSON.stringify(params),
53
  });
54
-
55
  if (response.ok) {
56
  const { article } = await response.json();
57
  onSourceArticleChange(article);
@@ -95,7 +96,7 @@ export function SourceArticle({
95
  </div>
96
  </div>
97
  </CardHeader>
98
-
99
  <CardContent className="h-[calc(100%-80px)]">
100
  <Textarea
101
  value={sourceArticle}
 
8
  import { Textarea } from '@/components/ui/textarea';
9
  import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
10
 
11
+ interface ArticleProps {
12
  className?: string;
13
  sourceArticle: string;
14
  onSourceArticleChange: (text: string) => void;
 
16
  onSourceLockedChange: (locked: boolean) => void;
17
  }
18
 
19
+ export function Article({
20
  className = '',
21
  sourceArticle,
22
  onSourceArticleChange,
23
  isSourceLocked,
24
  onSourceLockedChange,
25
+ }: ArticleProps) {
26
+
27
  const [wordCount, setWordCount] = useState(0);
28
  const [isModalOpen, setIsModalOpen] = useState(false);
29
  const [isGeneratingArticle, setIsGeneratingArticle] = useState(false);
 
52
  headers: { 'Content-Type': 'application/json' },
53
  body: JSON.stringify(params),
54
  });
55
+
56
  if (response.ok) {
57
  const { article } = await response.json();
58
  onSourceArticleChange(article);
 
96
  </div>
97
  </div>
98
  </CardHeader>
99
+
100
  <CardContent className="h-[calc(100%-80px)]">
101
  <Textarea
102
  value={sourceArticle}
src/components/{source-article → article}/ArticleGenerationModal.tsx RENAMED
@@ -10,10 +10,10 @@ import {
10
  DialogFooter,
11
  } from '@/components/ui/dialog';
12
  import { Button } from '@/components/ui/button';
13
- import { Checkbox } from '@/components/ui/checkbox';
14
  import { Label } from '@/components/ui/label';
15
  import { Textarea } from '@/components/ui/textarea';
16
  import { ScrollArea } from '@/components/ui/scroll-area';
 
17
  import { Loader2 } from 'lucide-react';
18
 
19
  interface ArticleGenerationModalProps {
@@ -129,78 +129,37 @@ export function ArticleGenerationModal({
129
  Configure the parameters for article generation
130
  </DialogDescription>
131
  </DialogHeader>
132
-
133
  <ScrollArea className="h-[calc(90vh-200px)] pr-4">
134
  <form onSubmit={handleSubmit} className="space-y-6">
135
  <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
136
- {/* Publisher */}
137
- <div>
138
- <Label className="mb-2">出版社 (Publisher)</Label>
139
- <div className="space-y-2">
140
- {PUBLISHER_OPTIONS.map(option => (
141
- <div key={option} className="flex items-center space-x-2">
142
- <Checkbox
143
- id={`publisher-${option}`}
144
- checked={formData.publisher.includes(option)}
145
- onCheckedChange={() => handleCheckboxChange('publisher', option)}
146
- />
147
- <Label
148
- htmlFor={`publisher-${option}`}
149
- className="text-sm font-normal cursor-pointer"
150
- >
151
- {option}
152
- </Label>
153
- </div>
154
- ))}
155
- </div>
156
- </div>
157
-
158
- {/* Grade */}
159
- <div>
160
- <Label className="mb-2">學生年級 (Grade Level)</Label>
161
- <div className="space-y-2">
162
- {GRADE_OPTIONS.map(option => (
163
- <div key={option} className="flex items-center space-x-2">
164
- <Checkbox
165
- id={`grade-${option}`}
166
- checked={formData.grade.includes(option)}
167
- onCheckedChange={() => handleCheckboxChange('grade', option)}
168
- />
169
- <Label
170
- htmlFor={`grade-${option}`}
171
- className="text-sm font-normal cursor-pointer"
172
- >
173
- {option}
174
- </Label>
175
- </div>
176
- ))}
177
- </div>
178
- </div>
179
  </div>
180
 
181
- {/* Unit */}
182
- <div>
183
- <Label className="mb-2">課程範圍 (Unit Range)</Label>
184
- <div className="grid grid-cols-3 gap-2">
185
- {UNIT_OPTIONS.map(option => (
186
- <div key={option} className="flex items-center space-x-2">
187
- <Checkbox
188
- id={`unit-${option}`}
189
- checked={formData.unit.includes(option)}
190
- onCheckedChange={() => handleCheckboxChange('unit', option)}
191
- />
192
- <Label
193
- htmlFor={`unit-${option}`}
194
- className="text-sm font-normal cursor-pointer"
195
- >
196
- {option}
197
- </Label>
198
- </div>
199
- ))}
200
- </div>
201
- </div>
202
 
203
- {/* Textbook Vocabulary */}
204
  <div>
205
  <Label htmlFor="textbook-vocab" className="mb-2">
206
  課本單字列表 (Textbook Vocabulary)
@@ -213,8 +172,6 @@ export function ArticleGenerationModal({
213
  className="bg-muted"
214
  />
215
  </div>
216
-
217
- {/* Additional Vocabulary */}
218
  <div>
219
  <Label htmlFor="additional-vocab" className="mb-2">
220
  額外單字列表 (Additional Vocabulary)
@@ -228,52 +185,24 @@ export function ArticleGenerationModal({
228
  </div>
229
 
230
  <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
231
- {/* Grammar */}
232
- <div>
233
- <Label className="mb-2">文法範圍 (Grammar Focus)</Label>
234
- <div className="space-y-2">
235
- {GRAMMAR_OPTIONS.map(option => (
236
- <div key={option} className="flex items-center space-x-2">
237
- <Checkbox
238
- id={`grammar-${option}`}
239
- checked={formData.grammar.includes(option)}
240
- onCheckedChange={() => handleCheckboxChange('grammar', option)}
241
- />
242
- <Label
243
- htmlFor={`grammar-${option}`}
244
- className="text-sm font-normal cursor-pointer"
245
- >
246
- {option}
247
- </Label>
248
- </div>
249
- ))}
250
- </div>
251
- </div>
252
-
253
- {/* Topic */}
254
- <div>
255
- <Label className="mb-2">主題範圍 (Topic)</Label>
256
- <div className="space-y-2">
257
- {TOPIC_OPTIONS.map(option => (
258
- <div key={option} className="flex items-center space-x-2">
259
- <Checkbox
260
- id={`topic-${option}`}
261
- checked={formData.topic.includes(option)}
262
- onCheckedChange={() => handleCheckboxChange('topic', option)}
263
- />
264
- <Label
265
- htmlFor={`topic-${option}`}
266
- className="text-sm font-normal cursor-pointer"
267
- >
268
- {option}
269
- </Label>
270
- </div>
271
- ))}
272
- </div>
273
- </div>
274
  </div>
275
 
276
- {/* Input Article */}
277
  <div>
278
  <Label htmlFor="input-article" className="mb-2">
279
  初始文章 (Initial Article)
@@ -293,8 +222,8 @@ export function ArticleGenerationModal({
293
  <Button type="button" variant="outline" onClick={onClose}>
294
  Cancel
295
  </Button>
296
- <Button
297
- type="submit"
298
  onClick={handleSubmit}
299
  disabled={isGenerating}
300
  >
 
10
  DialogFooter,
11
  } from '@/components/ui/dialog';
12
  import { Button } from '@/components/ui/button';
 
13
  import { Label } from '@/components/ui/label';
14
  import { Textarea } from '@/components/ui/textarea';
15
  import { ScrollArea } from '@/components/ui/scroll-area';
16
+ import { CheckboxField } from '@/components/ui/checkbox-field';
17
  import { Loader2 } from 'lucide-react';
18
 
19
  interface ArticleGenerationModalProps {
 
129
  Configure the parameters for article generation
130
  </DialogDescription>
131
  </DialogHeader>
132
+
133
  <ScrollArea className="h-[calc(90vh-200px)] pr-4">
134
  <form onSubmit={handleSubmit} className="space-y-6">
135
  <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
136
+ <CheckboxField
137
+ label="出版社 (Publisher)"
138
+ fieldName="publisher"
139
+ value={formData.publisher}
140
+ onChange={(value: string) => handleCheckboxChange('publisher', value)}
141
+ options={PUBLISHER_OPTIONS}
142
+ columns={1}
143
+ />
144
+ <CheckboxField
145
+ label="學生年級 (Grade Level)"
146
+ fieldName="grade"
147
+ value={formData.grade}
148
+ onChange={(value: string) => handleCheckboxChange('grade', value)}
149
+ options={GRADE_OPTIONS}
150
+ columns={1}
151
+ />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
152
  </div>
153
 
154
+ <CheckboxField
155
+ label="課程範圍 (Unit Range)"
156
+ fieldName="unit"
157
+ value={formData.unit}
158
+ onChange={(value: string) => handleCheckboxChange('unit', value)}
159
+ options={UNIT_OPTIONS}
160
+ columns={3}
161
+ />
 
 
 
 
 
 
 
 
 
 
 
 
 
162
 
 
163
  <div>
164
  <Label htmlFor="textbook-vocab" className="mb-2">
165
  課本單字列表 (Textbook Vocabulary)
 
172
  className="bg-muted"
173
  />
174
  </div>
 
 
175
  <div>
176
  <Label htmlFor="additional-vocab" className="mb-2">
177
  額外單字列表 (Additional Vocabulary)
 
185
  </div>
186
 
187
  <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
188
+ <CheckboxField
189
+ label="文法範圍 (Grammar Focus)"
190
+ fieldName="grammar"
191
+ value={formData.grammar}
192
+ onChange={(value: string) => handleCheckboxChange('grammar', value)}
193
+ options={GRAMMAR_OPTIONS}
194
+ columns={1}
195
+ />
196
+ <CheckboxField
197
+ label="主題範圍 (Topic)"
198
+ fieldName="topic"
199
+ value={formData.topic}
200
+ onChange={(value: string) => handleCheckboxChange('topic', value)}
201
+ options={TOPIC_OPTIONS}
202
+ columns={1}
203
+ />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
204
  </div>
205
 
 
206
  <div>
207
  <Label htmlFor="input-article" className="mb-2">
208
  初始文章 (Initial Article)
 
222
  <Button type="button" variant="outline" onClick={onClose}>
223
  Cancel
224
  </Button>
225
+ <Button
226
+ type="submit"
227
  onClick={handleSubmit}
228
  disabled={isGenerating}
229
  >
src/components/{source-article → article}/index.ts RENAMED
@@ -1,2 +1,2 @@
1
- export { SourceArticle } from './SourceArticle';
2
  export { ArticleGenerationModal } from './ArticleGenerationModal';
 
1
+ export { Article } from './Article';
2
  export { ArticleGenerationModal } from './ArticleGenerationModal';
src/components/ui/checkbox-field.tsx ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Label } from '@/components/ui/label';
2
+ import { CheckboxGrid } from '@/components/ui/checkbox-grid';
3
+
4
+ interface CheckboxFieldProps {
5
+ label: string;
6
+ fieldName: string;
7
+ value: string[];
8
+ onChange: (value: string) => void;
9
+ options: string[];
10
+ columns?: 1 | 2 | 3 | 4;
11
+ className?: string;
12
+ }
13
+
14
+ export function CheckboxField({
15
+ label,
16
+ fieldName,
17
+ value,
18
+ onChange,
19
+ options,
20
+ columns = 1,
21
+ className = ''
22
+ }: CheckboxFieldProps) {
23
+ return (
24
+ <div className={className}>
25
+ <Label className="mb-2">{label}</Label>
26
+ <CheckboxGrid
27
+ options={options}
28
+ selectedValues={value}
29
+ onSelectionChange={onChange}
30
+ fieldName={fieldName}
31
+ columns={columns}
32
+ />
33
+ </div>
34
+ );
35
+ }
src/components/ui/checkbox-grid.tsx ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Checkbox } from '@/components/ui/checkbox';
2
+ import { Label } from '@/components/ui/label';
3
+
4
+ interface CheckboxGridProps {
5
+ options: string[];
6
+ selectedValues: string[];
7
+ onSelectionChange: (value: string) => void;
8
+ fieldName: string;
9
+ columns?: 1 | 2 | 3 | 4;
10
+ className?: string;
11
+ }
12
+
13
+ export function CheckboxGrid({
14
+ options,
15
+ selectedValues,
16
+ onSelectionChange,
17
+ fieldName,
18
+ columns = 1,
19
+ className = ''
20
+ }: CheckboxGridProps) {
21
+ return (
22
+ <div className={`grid grid-cols-${columns} gap-2 ${className}`}>
23
+ {options.map(option => (
24
+ <div key={option} className="flex items-center space-x-2">
25
+ <Checkbox
26
+ id={`${fieldName}-${option}`}
27
+ checked={selectedValues.includes(option)}
28
+ onCheckedChange={() => onSelectionChange(option)}
29
+ />
30
+ <Label
31
+ htmlFor={`${fieldName}-${option}`}
32
+ className="text-sm font-normal cursor-pointer"
33
+ >
34
+ {option}
35
+ </Label>
36
+ </div>
37
+ ))}
38
+ </div>
39
+ );
40
+ }
41
+
src/components/ui/checkbox-group.tsx ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Label } from '@/components/ui/label';
2
+ import { CheckboxGrid } from '@/components/ui/checkbox-grid';
3
+
4
+ interface CheckboxFieldProps {
5
+ label: string;
6
+ fieldName: string;
7
+ value: string[];
8
+ onChange: (value: string) => void;
9
+ options: string[];
10
+ columns?: 1 | 2 | 3 | 4;
11
+ className?: string;
12
+ }
13
+
14
+ export function CheckboxField({
15
+ label,
16
+ fieldName,
17
+ value,
18
+ onChange,
19
+ options,
20
+ columns = 1,
21
+ className = ''
22
+ }: CheckboxFieldProps) {
23
+ return (
24
+ <div className={className}>
25
+ <Label className="mb-2">{label}</Label>
26
+ <CheckboxGrid
27
+ options={options}
28
+ selectedValues={value}
29
+ onSelectionChange={onChange}
30
+ fieldName={fieldName}
31
+ columns={columns}
32
+ />
33
+ </div>
34
+ );
35
+ }
src/config/ai.ts ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ export const AI_CONFIG = {
2
+ model: 'gpt-4o-mini',
3
+ temperature: 0.7,
4
+ maxTokens: 1000,
5
+ } as const;
src/lib/langfuse.ts ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { LangfuseClient } from "@langfuse/client";
2
+
3
+ // Ensure this is never imported on the client
4
+ if (typeof window !== "undefined") {
5
+ throw new Error("`langfuse` must only be used on the server side.");
6
+ }
7
+
8
+ declare global {
9
+ // eslint-disable-next-line no-var
10
+ var __langfuse__: LangfuseClient | undefined;
11
+ }
12
+
13
+ export const langfuse = globalThis.__langfuse__ ?? new LangfuseClient({
14
+ publicKey: process.env.LANGFUSE_PUBLIC_KEY,
15
+ secretKey: process.env.LANGFUSE_SECRET_KEY,
16
+ baseUrl: process.env.LANGFUSE_BASE_URL || "https://cloud.langfuse.com",
17
+ });
18
+
19
+ if (!globalThis.__langfuse__) globalThis.__langfuse__ = langfuse;