@nextsparkjs/plugin-ai 0.1.0-beta.1 → 0.1.0-beta.100

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.
@@ -9,6 +9,7 @@ import { NextRequest, NextResponse } from 'next/server'
9
9
  import { selectModel, calculateCost, validatePlugin, extractTokens, handleAIError } from '../../lib/core-utils'
10
10
  import { getServerPluginConfig } from '../../lib/server-env'
11
11
  import { authenticateRequest } from '@nextsparkjs/core/lib/api/auth/dual-auth'
12
+ import { withRateLimitTier } from '@nextsparkjs/core/lib/api/rate-limit'
12
13
  import { generateText } from 'ai'
13
14
  import { saveExampleSafely } from '../../lib/save-example'
14
15
  import { z } from 'zod'
@@ -25,7 +26,7 @@ const GenerateRequestSchema = z.object({
25
26
  saveExample: z.boolean().optional().default(false)
26
27
  })
27
28
 
28
- export async function POST(request: NextRequest) {
29
+ const postHandler = async (request: NextRequest) => {
29
30
  try {
30
31
  // 1. Authentication
31
32
  const authResult = await authenticateRequest(request)
@@ -79,20 +80,25 @@ export async function POST(request: NextRequest) {
79
80
 
80
81
  // 8. Save example if requested (opt-in)
81
82
  if (saveExample) {
83
+ const teamId = request.headers.get('x-team-id') || ''
82
84
  await saveExampleSafely(
83
85
  {
84
86
  prompt,
85
87
  response: result.text,
86
88
  model: selectedModel.modelName,
87
89
  status: 'completed',
90
+ operation: 'generate',
91
+ provider: selectedModel.provider,
88
92
  metadata: {
89
93
  tokens: tokens.total,
94
+ tokensInput: tokens.input,
95
+ tokensOutput: tokens.output,
90
96
  cost,
91
- provider: selectedModel.provider,
92
97
  isLocal: selectedModel.isLocal
93
98
  }
94
99
  },
95
- authResult.user!.id
100
+ authResult.user!.id,
101
+ teamId
96
102
  )
97
103
  }
98
104
 
@@ -119,10 +125,12 @@ export async function POST(request: NextRequest) {
119
125
  }
120
126
  }
121
127
 
128
+ export const POST = withRateLimitTier(postHandler, 'write')
129
+
122
130
  /**
123
131
  * Get endpoint info
124
132
  */
125
- export async function GET(): Promise<NextResponse> {
133
+ const getHandler = async (): Promise<NextResponse> => {
126
134
  const config = await getServerPluginConfig()
127
135
 
128
136
  return NextResponse.json({
@@ -157,4 +165,6 @@ export async function GET(): Promise<NextResponse> {
157
165
  anthropic: 'Add ANTHROPIC_API_KEY to contents/plugins/ai/.env'
158
166
  }
159
167
  })
160
- }
168
+ }
169
+
170
+ export const GET = withRateLimitTier(getHandler, 'read')
@@ -81,6 +81,7 @@ import { AIHistoryService } from '@/contents/plugins/ai/lib/ai-history-service'
81
81
  ```typescript
82
82
  const historyId = await AIHistoryService.startOperation({
83
83
  userId: 'user-id',
84
+ teamId: 'team-id',
84
85
  operation: 'generate',
85
86
  model: 'gpt-4o-mini',
86
87
  provider: 'openai',
@@ -133,6 +134,7 @@ await AIHistoryService.failOperation({
133
134
  ```typescript
134
135
  {
135
136
  userId: string // Required
137
+ teamId: string // Required - team context for the operation
136
138
  operation: AIOperation // 'generate' | 'refine' | 'analyze' | 'chat' | 'completion' | 'other'
137
139
  model: string // Model name (e.g., 'gpt-4o-mini')
138
140
  provider?: AIProvider // 'openai' | 'anthropic' | 'ollama'
@@ -147,6 +149,7 @@ await AIHistoryService.failOperation({
147
149
  ```typescript
148
150
  const historyId = await AIHistoryService.startOperation({
149
151
  userId: session.user.id,
152
+ teamId: session.user.activeTeamId,
150
153
  operation: 'generate',
151
154
  model: 'llama3.2:3b',
152
155
  provider: 'ollama'
@@ -225,6 +228,7 @@ try {
225
228
  ```typescript
226
229
  const historyId = await AIHistoryService.startOperation({
227
230
  userId: 'user-id',
231
+ teamId: 'team-id',
228
232
  operation: 'analyze',
229
233
  model: 'claude-3-5-haiku-20241022',
230
234
  relatedEntityType: 'clients',
@@ -249,6 +253,7 @@ PATCH /api/v1/plugin/ai/ai-history/:id
249
253
  // Generate product description
250
254
  const historyId = await AIHistoryService.startOperation({
251
255
  userId: session.user.id,
256
+ teamId: session.user.activeTeamId,
252
257
  operation: 'generate',
253
258
  model: 'gpt-4o-mini',
254
259
  relatedEntityType: 'products',
@@ -265,6 +270,7 @@ const historyId = await AIHistoryService.startOperation({
265
270
  // Analyze client data
266
271
  const historyId = await AIHistoryService.startOperation({
267
272
  userId: session.user.id,
273
+ teamId: session.user.activeTeamId,
268
274
  operation: 'analyze',
269
275
  model: 'claude-3-5-sonnet-20241022',
270
276
  relatedEntityType: 'clients',
@@ -279,6 +285,7 @@ const historyId = await AIHistoryService.startOperation({
279
285
  // Audit article content
280
286
  const historyId = await AIHistoryService.startOperation({
281
287
  userId: session.user.id,
288
+ teamId: session.user.activeTeamId,
282
289
  operation: 'analyze',
283
290
  model: 'gpt-4o',
284
291
  relatedEntityType: 'articles',
@@ -384,6 +391,7 @@ export async function generateProductDescription(productId: string) {
384
391
  // 1. Start operation tracking
385
392
  const historyId = await AIHistoryService.startOperation({
386
393
  userId: session.user.id,
394
+ teamId: session.user.activeTeamId,
387
395
  operation: 'generate',
388
396
  model: 'gpt-4o-mini',
389
397
  provider: 'openai',
@@ -336,6 +336,7 @@ export async function generateAndSaveDescription(productId: string) {
336
336
  // Start operation tracking
337
337
  const historyId = await AIHistoryService.startOperation({
338
338
  userId: session.user.id,
339
+ teamId: session.user.activeTeamId,
339
340
  operation: 'generate',
340
341
  model: 'gpt-4o-mini',
341
342
  provider: 'openai',
@@ -17,7 +17,8 @@ export const aiHistoryEntityConfig: EntityConfig = {
17
17
  // ==========================================
18
18
  // 1. BASIC IDENTIFICATION
19
19
  // ==========================================
20
- slug: 'ai-history', // Single source of truth - derives tableName, apiPath, metaTableName, i18nNamespace
20
+ slug: 'ai-history',
21
+ tableName: 'ai_history', // Explicit table name (underscores, not hyphens)
21
22
  enabled: true,
22
23
  names: {
23
24
  singular: 'AI History',
@@ -57,46 +58,7 @@ export const aiHistoryEntityConfig: EntityConfig = {
57
58
  },
58
59
 
59
60
  // ==========================================
60
- // 4. PERMISSIONS SYSTEM
61
- // ==========================================
62
- permissions: {
63
- actions: [
64
- {
65
- action: 'create',
66
- label: 'Create AI history',
67
- description: 'Can create AI history entries (typically via API)',
68
- roles: ['owner', 'admin', 'member'],
69
- },
70
- {
71
- action: 'read',
72
- label: 'View AI history',
73
- description: 'Can view AI history entries (users see their own via API filtering)',
74
- roles: ['owner', 'admin', 'member'],
75
- },
76
- {
77
- action: 'list',
78
- label: 'List AI history',
79
- description: 'Can list AI history entries',
80
- roles: ['owner', 'admin', 'member'],
81
- },
82
- {
83
- action: 'update',
84
- label: 'Edit AI history',
85
- description: 'Can modify AI history entries (immutable for most users)',
86
- roles: ['owner', 'admin'],
87
- },
88
- {
89
- action: 'delete',
90
- label: 'Delete AI history',
91
- description: 'Can delete AI history entries (users can delete their own)',
92
- roles: ['owner', 'admin', 'member'],
93
- dangerous: true,
94
- },
95
- ],
96
- },
97
-
98
- // ==========================================
99
- // 5. INTERNATIONALIZATION
61
+ // 4. INTERNATIONALIZATION
100
62
  // ==========================================
101
63
  i18n: {
102
64
  fallbackLocale: 'en',
@@ -308,6 +308,25 @@ export const aiHistoryFields: EntityField[] = [
308
308
  readOnly: true,
309
309
  },
310
310
  },
311
+ {
312
+ name: 'updatedAt',
313
+ type: 'datetime',
314
+ required: false,
315
+ display: {
316
+ label: 'Updated At',
317
+ description: 'When the record was last updated',
318
+ showInList: false,
319
+ showInDetail: true,
320
+ showInForm: false,
321
+ order: 98,
322
+ columnWidth: 6,
323
+ },
324
+ api: {
325
+ searchable: false,
326
+ sortable: true,
327
+ readOnly: true,
328
+ },
329
+ },
311
330
  {
312
331
  name: 'completedAt',
313
332
  type: 'datetime',
@@ -318,7 +337,7 @@ export const aiHistoryFields: EntityField[] = [
318
337
  showInList: false,
319
338
  showInDetail: true,
320
339
  showInForm: false,
321
- order: 98,
340
+ order: 99,
322
341
  columnWidth: 6,
323
342
  },
324
343
  api: {
@@ -1,7 +1,7 @@
1
1
  -- =============================================
2
2
  -- AI HISTORY ENTITY MIGRATION - PLUGIN ENTITY
3
3
  -- Plugin: AI
4
- -- Entity: ai-history
4
+ -- Entity: ai_history
5
5
  -- Generic AI interaction history for all plugins
6
6
  -- Date: 2025-10-03
7
7
  -- =============================================
@@ -15,6 +15,7 @@ DROP TABLE IF EXISTS "ai_history" CASCADE;
15
15
  CREATE TABLE "ai_history" (
16
16
  id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
17
17
  "userId" TEXT NOT NULL REFERENCES "users"(id) ON DELETE CASCADE,
18
+ "teamId" TEXT NOT NULL REFERENCES "teams"(id) ON DELETE CASCADE,
18
19
 
19
20
  -- Generic polymorphic relationship (flexible for any future use case)
20
21
  "relatedEntityType" TEXT,
@@ -41,6 +42,7 @@ CREATE TABLE "ai_history" (
41
42
 
42
43
  -- Timestamps
43
44
  "createdAt" TIMESTAMPTZ NOT NULL DEFAULT now(),
45
+ "updatedAt" TIMESTAMPTZ NOT NULL DEFAULT now(),
44
46
  "completedAt" TIMESTAMPTZ
45
47
  );
46
48
 
@@ -49,6 +51,8 @@ CREATE TABLE "ai_history" (
49
51
  -- =============================================
50
52
 
51
53
  CREATE INDEX IF NOT EXISTS idx_ai_history_user_id ON "ai_history"("userId");
54
+ CREATE INDEX IF NOT EXISTS idx_ai_history_team_id ON "ai_history"("teamId");
55
+ CREATE INDEX IF NOT EXISTS idx_ai_history_team_user ON "ai_history"("teamId", "userId");
52
56
  CREATE INDEX IF NOT EXISTS idx_ai_history_entity ON "ai_history"("relatedEntityType", "relatedEntityId") WHERE "relatedEntityType" IS NOT NULL;
53
57
  CREATE INDEX IF NOT EXISTS idx_ai_history_status ON "ai_history"(status);
54
58
  CREATE INDEX IF NOT EXISTS idx_ai_history_operation ON "ai_history"(operation);
@@ -103,6 +107,13 @@ CREATE POLICY "Admins can manage all ai history" ON "ai_history"
103
107
  )
104
108
  );
105
109
 
110
+ -- =============================================
111
+ -- TRIGGER: Auto-update updatedAt
112
+ -- =============================================
113
+ CREATE TRIGGER ai_history_set_updated_at
114
+ BEFORE UPDATE ON "ai_history"
115
+ FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
116
+
106
117
  -- =============================================
107
118
  -- TRIGGER: Auto-update completedAt
108
119
  -- =============================================
@@ -152,6 +163,7 @@ COMMENT ON COLUMN "ai_history"."estimatedCost" IS 'Cost in USD based on model pr
152
163
  COMMENT ON COLUMN "ai_history"."balanceAfter" IS 'User credit balance after this operation';
153
164
  COMMENT ON COLUMN "ai_history"."errorMessage" IS 'Error message if operation failed';
154
165
  COMMENT ON COLUMN "ai_history"."createdAt" IS 'When the AI operation was initiated';
166
+ COMMENT ON COLUMN "ai_history"."updatedAt" IS 'When the record was last updated';
155
167
  COMMENT ON COLUMN "ai_history"."completedAt" IS 'When the AI operation finished (success or failure)';
156
168
 
157
169
  -- =============================================
@@ -1,7 +1,7 @@
1
1
  -- =============================================
2
2
  -- AI HISTORY METADATA SYSTEM
3
3
  -- Plugin: AI
4
- -- Entity: ai-history
4
+ -- Entity: ai_history
5
5
  -- =============================================
6
6
  --
7
7
  -- This migration adds comprehensive metadata capabilities through ai_history_metas table
@@ -14,27 +14,6 @@ import { CreateMetaPayload } from '@nextsparkjs/core/types/meta.types';
14
14
  * METADATA BY ENDPOINT (Standardized)
15
15
  * ============================================
16
16
  *
17
- * analyze-brief (/api/v1/theme/content-buddy/analyze-brief):
18
- * - briefText: Original brief text (for auditing/debugging)
19
- * - briefLength: Length of brief text
20
- * - extractedFieldsCount: Number of fields extracted
21
- * - audiencesCount: Number of audience entities extracted
22
- * - objectivesCount: Number of objective entities extracted
23
- * - productsCount: Number of product entities extracted
24
- * - totalChildEntities: Total child entities extracted
25
- * - clientName: Extracted client name
26
- * - clientIndustry: Extracted client industry
27
- *
28
- * generate-content (/api/v1/theme/content-buddy/generate-content):
29
- * Core generation parameters (reproducibility):
30
- * - temperature: AI model temperature used
31
- * - tone: Content tone
32
- * - brandVoice: Brand voice style
33
- * - audience: Target audience description
34
- * - topic: Content topic
35
- * - postType: Type of post
36
- * - language: Content language code (e.g., "en", "es")
37
- *
38
17
  * Content specifications (analytics):
39
18
  * - wordCount: Target word count
40
19
  * - keywords: Comma-separated keywords (e.g., "innovation,tech,ai")
@@ -54,13 +33,6 @@ import { CreateMetaPayload } from '@nextsparkjs/core/types/meta.types';
54
33
  * - objectiveId: Marketing objective (if specified)
55
34
  * - targetAudienceId: Specific audience segment (if specified)
56
35
  *
57
- * refine-content (/api/v1/theme/content-buddy/refine-content):
58
- * - userInstruction: User's refinement instruction
59
- * - sourceOperationId: UUID linking to source operation (for workflow tracking)
60
- * - temperature: AI model temperature used
61
- * - tone: Desired tone (if specified)
62
- * - platform: Social media platform being refined for
63
- *
64
36
  * ============================================
65
37
  * DEPRECATED METADATA (Use ai_history columns instead)
66
38
  * ============================================
@@ -20,6 +20,7 @@ export type AIStatus = 'pending' | 'processing' | 'completed' | 'failed'
20
20
 
21
21
  export interface StartOperationParams {
22
22
  userId: string
23
+ teamId: string // Required - team context for the operation
23
24
  operation: AIOperation
24
25
  model: string
25
26
  provider?: AIProvider
@@ -75,6 +76,7 @@ export class AIHistoryService {
75
76
  static async startOperation(params: StartOperationParams): Promise<string> {
76
77
  const {
77
78
  userId,
79
+ teamId,
78
80
  operation,
79
81
  model,
80
82
  provider = 'anthropic',
@@ -86,6 +88,7 @@ export class AIHistoryService {
86
88
  const result = await queryOne<{ id: string }>(
87
89
  `INSERT INTO "ai_history" (
88
90
  "userId",
91
+ "teamId",
89
92
  operation,
90
93
  model,
91
94
  provider,
@@ -93,9 +96,9 @@ export class AIHistoryService {
93
96
  "relatedEntityId",
94
97
  status,
95
98
  "createdAt"
96
- ) VALUES ($1, $2, $3, $4, $5, $6, $7, now())
99
+ ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, now())
97
100
  RETURNING id`,
98
- [userId, operation, model, provider, relatedEntityType || null, relatedEntityId || null, 'pending']
101
+ [userId, teamId, operation, model, provider, relatedEntityType || null, relatedEntityId || null, 'pending']
99
102
  )
100
103
 
101
104
  if (!result) {
@@ -1,28 +1,31 @@
1
1
  /**
2
- * AI Example Saving - PostgreSQL Implementation
2
+ * AI History Saving - PostgreSQL Implementation
3
3
  *
4
4
  * Uses project's PostgreSQL connection with RLS support.
5
- * Self-contained plugin with direct DB access.
5
+ * Saves AI interactions to the ai_history table.
6
6
  */
7
7
 
8
8
  import { mutateWithRLS, queryWithRLS } from '@nextsparkjs/core/lib/db'
9
9
  import { sanitizePrompt, sanitizeResponse } from './sanitize'
10
10
 
11
11
  export interface AIExampleData {
12
- title?: string
13
12
  prompt: string
14
13
  response: string
15
14
  model: string
16
15
  status: 'pending' | 'processing' | 'completed' | 'failed'
17
- userId?: string
16
+ operation?: 'generate' | 'refine' | 'analyze' | 'chat' | 'completion' | 'other'
17
+ provider?: string
18
18
  metadata?: {
19
19
  tokens?: number
20
+ tokensInput?: number
21
+ tokensOutput?: number
20
22
  cost?: number
21
23
  duration?: number
22
- provider?: string
23
24
  isLocal?: boolean
24
25
  platforms?: number
25
26
  imagesProcessed?: number
27
+ relatedEntityType?: string
28
+ relatedEntityId?: string
26
29
  }
27
30
  }
28
31
 
@@ -34,11 +37,12 @@ export interface SaveExampleResult {
34
37
  }
35
38
 
36
39
  /**
37
- * Save AI example to PostgreSQL database
40
+ * Save AI interaction to ai_history table
38
41
  */
39
42
  export async function saveAIExample(
40
43
  data: AIExampleData,
41
- userId: string
44
+ userId: string,
45
+ teamId: string
42
46
  ): Promise<SaveExampleResult> {
43
47
  try {
44
48
  // 1. Sanitize sensitive data
@@ -49,25 +53,35 @@ export async function saveAIExample(
49
53
  sanitizedPrompt !== data.prompt ||
50
54
  sanitizedResponse !== data.response
51
55
 
52
- // 2. Generate title if not provided
53
- const title = data.title || generateTitle(sanitizedPrompt)
56
+ // 2. Extract token info
57
+ const tokensInput = data.metadata?.tokensInput || 0
58
+ const tokensOutput = data.metadata?.tokensOutput || 0
59
+ const tokensUsed = data.metadata?.tokens || (tokensInput + tokensOutput)
54
60
 
55
- // 3. Insert into database with RLS
61
+ // 3. Insert into ai_history table with RLS
56
62
  const query = `
57
- INSERT INTO ai_example (
58
- title, prompt, response, model, status, "userId", metadata, "createdAt", "updatedAt"
59
- ) VALUES ($1, $2, $3, $4, $5, $6, $7, NOW(), NOW())
63
+ INSERT INTO ai_history (
64
+ "userId", "teamId", operation, model, provider, status,
65
+ "tokensUsed", "tokensInput", "tokensOutput", "estimatedCost",
66
+ "relatedEntityType", "relatedEntityId",
67
+ "createdAt", "updatedAt"
68
+ ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, NOW(), NOW())
60
69
  RETURNING id
61
70
  `
62
71
 
63
72
  const params = [
64
- title,
65
- sanitizedPrompt,
66
- sanitizedResponse,
73
+ userId,
74
+ teamId,
75
+ data.operation || 'generate',
67
76
  data.model,
77
+ data.provider || 'ollama',
68
78
  data.status,
69
- userId,
70
- JSON.stringify(data.metadata || {})
79
+ tokensUsed || null,
80
+ tokensInput || null,
81
+ tokensOutput || null,
82
+ data.metadata?.cost || null,
83
+ data.metadata?.relatedEntityType || null,
84
+ data.metadata?.relatedEntityId || null
71
85
  ]
72
86
 
73
87
  const result = await mutateWithRLS<{ id: string }>(query, params, userId)
@@ -75,13 +89,14 @@ export async function saveAIExample(
75
89
  if (result.rowCount === 0 || !result.rows[0]) {
76
90
  return {
77
91
  success: false,
78
- error: 'Failed to insert example'
92
+ error: 'Failed to insert ai_history record'
79
93
  }
80
94
  }
81
95
 
82
- console.log('[AI Plugin] Example saved to database:', {
96
+ console.log('[AI Plugin] History saved to database:', {
83
97
  id: result.rows[0].id,
84
- title,
98
+ operation: data.operation || 'generate',
99
+ model: data.model,
85
100
  sanitizationApplied
86
101
  })
87
102
 
@@ -92,7 +107,7 @@ export async function saveAIExample(
92
107
  }
93
108
 
94
109
  } catch (error) {
95
- console.error('[AI Plugin] Save example failed:', error)
110
+ console.error('[AI Plugin] Save history failed:', error)
96
111
  return {
97
112
  success: false,
98
113
  error: error instanceof Error ? error.message : 'Unknown error'
@@ -101,29 +116,44 @@ export async function saveAIExample(
101
116
  }
102
117
 
103
118
  /**
104
- * Get AI examples from PostgreSQL
119
+ * Get AI history from PostgreSQL
105
120
  */
106
121
  export async function getAIExamples(params: {
107
122
  userId?: string
123
+ teamId?: string
108
124
  limit?: number
109
125
  offset?: number
110
126
  status?: string
127
+ operation?: string
111
128
  }) {
112
129
  try {
113
- const { userId, limit = 50, offset = 0, status } = params
130
+ const { userId, teamId, limit = 50, offset = 0, status, operation } = params
114
131
 
115
132
  let query = `
116
- SELECT id, title, prompt, response, model, status, "userId", metadata, "createdAt", "updatedAt"
117
- FROM ai_example
133
+ SELECT id, "userId", "teamId", operation, model, provider, status,
134
+ "tokensUsed", "tokensInput", "tokensOutput", "estimatedCost",
135
+ "relatedEntityType", "relatedEntityId",
136
+ "createdAt", "updatedAt", "completedAt"
137
+ FROM ai_history
118
138
  WHERE 1=1
119
139
  `
120
140
  const queryParams: (string | number)[] = []
121
141
 
142
+ if (teamId) {
143
+ queryParams.push(teamId)
144
+ query += ` AND "teamId" = $${queryParams.length}`
145
+ }
146
+
122
147
  if (status) {
123
148
  queryParams.push(status)
124
149
  query += ` AND status = $${queryParams.length}`
125
150
  }
126
151
 
152
+ if (operation) {
153
+ queryParams.push(operation)
154
+ query += ` AND operation = $${queryParams.length}`
155
+ }
156
+
127
157
  query += ` ORDER BY "createdAt" DESC LIMIT $${queryParams.length + 1} OFFSET $${queryParams.length + 2}`
128
158
  queryParams.push(limit, offset)
129
159
 
@@ -134,7 +164,7 @@ export async function getAIExamples(params: {
134
164
  data: rows
135
165
  }
136
166
  } catch (error) {
137
- console.error('[AI Plugin] Get examples failed:', error)
167
+ console.error('[AI Plugin] Get history failed:', error)
138
168
  return {
139
169
  success: false,
140
170
  error: error instanceof Error ? error.message : 'Unknown error',
@@ -144,13 +174,16 @@ export async function getAIExamples(params: {
144
174
  }
145
175
 
146
176
  /**
147
- * Get single AI example by ID from PostgreSQL
177
+ * Get single AI history record by ID
148
178
  */
149
179
  export async function getAIExampleById(id: string, userId?: string) {
150
180
  try {
151
181
  const query = `
152
- SELECT id, title, prompt, response, model, status, "userId", metadata, "createdAt", "updatedAt"
153
- FROM ai_example
182
+ SELECT id, "userId", "teamId", operation, model, provider, status,
183
+ "tokensUsed", "tokensInput", "tokensOutput", "estimatedCost",
184
+ "relatedEntityType", "relatedEntityId",
185
+ "createdAt", "updatedAt", "completedAt"
186
+ FROM ai_history
154
187
  WHERE id = $1
155
188
  `
156
189
 
@@ -159,7 +192,7 @@ export async function getAIExampleById(id: string, userId?: string) {
159
192
  if (rows.length === 0) {
160
193
  return {
161
194
  success: false,
162
- error: 'Example not found'
195
+ error: 'History record not found'
163
196
  }
164
197
  }
165
198
 
@@ -168,7 +201,7 @@ export async function getAIExampleById(id: string, userId?: string) {
168
201
  data: rows[0]
169
202
  }
170
203
  } catch (error) {
171
- console.error('[AI Plugin] Get example by ID failed:', error)
204
+ console.error('[AI Plugin] Get history by ID failed:', error)
172
205
  return {
173
206
  success: false,
174
207
  error: error instanceof Error ? error.message : 'Unknown error'
@@ -177,18 +210,18 @@ export async function getAIExampleById(id: string, userId?: string) {
177
210
  }
178
211
 
179
212
  /**
180
- * Delete AI example from PostgreSQL
213
+ * Delete AI history record
181
214
  */
182
215
  export async function deleteAIExample(id: string, userId: string) {
183
216
  try {
184
- const query = `DELETE FROM ai_example WHERE id = $1 RETURNING id`
217
+ const query = `DELETE FROM ai_history WHERE id = $1 RETURNING id`
185
218
 
186
219
  const result = await mutateWithRLS<{ id: string }>(query, [id], userId)
187
220
 
188
221
  if (result.rowCount === 0) {
189
222
  return {
190
223
  success: false,
191
- error: 'Example not found or unauthorized'
224
+ error: 'History record not found or unauthorized'
192
225
  }
193
226
  }
194
227
 
@@ -197,7 +230,7 @@ export async function deleteAIExample(id: string, userId: string) {
197
230
  id: result.rows[0].id
198
231
  }
199
232
  } catch (error) {
200
- console.error('[AI Plugin] Delete example failed:', error)
233
+ console.error('[AI Plugin] Delete history failed:', error)
201
234
  return {
202
235
  success: false,
203
236
  error: error instanceof Error ? error.message : 'Unknown error'
@@ -206,32 +239,23 @@ export async function deleteAIExample(id: string, userId: string) {
206
239
  }
207
240
 
208
241
  /**
209
- * Generate title from prompt (helper)
210
- */
211
- function generateTitle(prompt: string): string {
212
- const cleaned = prompt.replace(/\n/g, ' ').trim()
213
- return cleaned.length > 50
214
- ? cleaned.substring(0, 50) + '...'
215
- : cleaned
216
- }
217
-
218
- /**
219
- * Save example with automatic error handling (fire-and-forget)
242
+ * Save history with automatic error handling (fire-and-forget)
220
243
  */
221
244
  export async function saveExampleSafely(
222
245
  data: AIExampleData,
223
- userId: string
246
+ userId: string,
247
+ teamId: string
224
248
  ): Promise<void> {
225
249
  try {
226
- const result = await saveAIExample(data, userId)
250
+ const result = await saveAIExample(data, userId, teamId)
227
251
 
228
252
  if (!result.success) {
229
- console.error('[AI Plugin] Example save failed:', result.error)
253
+ console.error('[AI Plugin] History save failed:', result.error)
230
254
  } else if (result.sanitizationApplied) {
231
- console.warn('[AI Plugin] Sanitization was applied to saved example')
255
+ console.warn('[AI Plugin] Sanitization was applied to saved history')
232
256
  }
233
257
  } catch (error) {
234
258
  // Silent fail - don't break the main request
235
- console.error('[AI Plugin] Example save error:', error)
259
+ console.error('[AI Plugin] History save error:', error)
236
260
  }
237
- }
261
+ }