AiBot ile ilgili çalışmalar
This commit is contained in:
parent
4b95b20a90
commit
741710fc72
4 changed files with 528 additions and 144 deletions
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"name": "Chat",
|
"name": "Chat SQL DataDictionary HTTP Stable v4",
|
||||||
"nodes": [
|
"nodes": [
|
||||||
{
|
{
|
||||||
"parameters": {
|
"parameters": {
|
||||||
|
|
@ -15,97 +15,277 @@
|
||||||
"type": "n8n-nodes-base.webhook",
|
"type": "n8n-nodes-base.webhook",
|
||||||
"typeVersion": 2,
|
"typeVersion": 2,
|
||||||
"position": [
|
"position": [
|
||||||
-704,
|
-900,
|
||||||
224
|
200
|
||||||
],
|
],
|
||||||
"id": "5624c28a-5e8f-4fd9-88ac-bc9a630a9cc0",
|
"id": "9f56ce73-26a2-4429-a77d-bfc6a91fe5d7",
|
||||||
"name": "Webhook",
|
"name": "Webhook"
|
||||||
"webhookId": "562dfd4f-4e0b-4292-9986-2fbd3b8ecdc9",
|
|
||||||
"alwaysOutputData": true,
|
|
||||||
"executeOnce": false,
|
|
||||||
"retryOnFail": false
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"promptType": "define",
|
"jsCode": "const question = $('Webhook').first().json.body?.question || '';\nreturn [{ json: { question, ollamaBody: { model: 'qwen2.5:3b', stream: false, options: { temperature: 0, top_p: 0.1, num_predict: 10 }, messages: [ { role: 'system', content: `Sen bir niyet sınıflandırma motorusun. Sadece CHAT veya SQL döndür. Kullanıcı veritabanından veri istiyorsa, listele/getir/say/toplam/raporla/sorgula/ara/filtrele gibi amaç varsa SQL döndür. Selamlaşma, açıklama, yardım, teşekkür, genel sohbet ise CHAT döndür. Açıklama yazma.` }, { role: 'user', content: question } ] } } }];"
|
||||||
"text": "={{ $('Webhook').item.json.body.question }}",
|
|
||||||
"options": {
|
|
||||||
"systemMessage": "{{ $json.body.description }}"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"type": "@n8n/n8n-nodes-langchain.agent",
|
|
||||||
"typeVersion": 1.8,
|
|
||||||
"position": [
|
|
||||||
-48,
|
|
||||||
224
|
|
||||||
],
|
|
||||||
"id": "f5a4eb80-6e3f-47a3-9e42-069fb6992a8b",
|
|
||||||
"name": "AI Chat",
|
|
||||||
"alwaysOutputData": false,
|
|
||||||
"onError": "continueErrorOutput"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"parameters": {
|
|
||||||
"options": {}
|
|
||||||
},
|
|
||||||
"type": "@n8n/n8n-nodes-langchain.memoryManager",
|
|
||||||
"typeVersion": 1.1,
|
|
||||||
"position": [
|
|
||||||
-448,
|
|
||||||
224
|
|
||||||
],
|
|
||||||
"id": "f008f540-216b-42f3-815c-b17658b4c7ad",
|
|
||||||
"name": "Chat Memory Manager",
|
|
||||||
"alwaysOutputData": false,
|
|
||||||
"disabled": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"parameters": {
|
|
||||||
"sessionIdType": "customKey",
|
|
||||||
"sessionKey": "={{ $('Webhook').item.json.body.sessionId }}",
|
|
||||||
"contextWindowLength": 100
|
|
||||||
},
|
|
||||||
"type": "@n8n/n8n-nodes-langchain.memoryBufferWindow",
|
|
||||||
"typeVersion": 1.3,
|
|
||||||
"position": [
|
|
||||||
-304,
|
|
||||||
480
|
|
||||||
],
|
|
||||||
"id": "80c637b6-06e1-41b1-a946-84065a9e327a",
|
|
||||||
"name": "Simple Memory"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"parameters": {
|
|
||||||
"options": {}
|
|
||||||
},
|
|
||||||
"type": "@n8n/n8n-nodes-langchain.lmChatGoogleGemini",
|
|
||||||
"typeVersion": 1,
|
|
||||||
"position": [
|
|
||||||
-48,
|
|
||||||
480
|
|
||||||
],
|
|
||||||
"id": "08879cec-78df-4bd7-a8aa-f0ec6fc5e0ae",
|
|
||||||
"name": "Google Gemini Chat Model",
|
|
||||||
"credentials": {
|
|
||||||
"googlePalmApi": {
|
|
||||||
"id": "2y8O0r5mQKG5cdcc",
|
|
||||||
"name": "Google Gemini(PaLM) Api account"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"parameters": {
|
|
||||||
"jsCode": "const cleanText = (text) =>\n text\n .replace(/\\\\/g, \"\\\\\\\\\") // ters slash\n .replace(/\"/g, '\\\\\"'); // çift tırnak\n\nreturn [\n {\n json: {\n type: \"chat\",\n question: $('Webhook').first().json.body.question,\n sql: null,\n answer: cleanText($input.first().json.output),\n chart: null,\n error: $input.first().json.error || null\n }\n }\n];\n\n"
|
|
||||||
},
|
},
|
||||||
"type": "n8n-nodes-base.code",
|
"type": "n8n-nodes-base.code",
|
||||||
"typeVersion": 2,
|
"typeVersion": 2,
|
||||||
"position": [
|
"position": [
|
||||||
560,
|
-680,
|
||||||
224
|
200
|
||||||
],
|
],
|
||||||
"id": "7f66cfbf-d19e-421b-92c3-d9a1909c1434",
|
"id": "e19a29c3-5833-4151-bd9c-d6c4cb49d2b0",
|
||||||
"name": "Chat Code",
|
"name": "Build Intent Request"
|
||||||
"alwaysOutputData": true
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"method": "POST",
|
||||||
|
"url": "http://ollama:11434/api/chat",
|
||||||
|
"sendBody": true,
|
||||||
|
"specifyBody": "json",
|
||||||
|
"jsonBody": "={{ $json.ollamaBody }}",
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.httpRequest",
|
||||||
|
"typeVersion": 4.2,
|
||||||
|
"position": [
|
||||||
|
-470,
|
||||||
|
200
|
||||||
|
],
|
||||||
|
"id": "2fddcd65-a682-4079-bbe1-67d63873d500",
|
||||||
|
"name": "Intent Ollama"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"jsCode": "const res = $input.first().json || {};\nconst content = String(res.message?.content || res.response || res.output || '').toUpperCase();\nconst question = $('Webhook').first().json.body?.question || '';\nconst tenantName = $('Webhook').first().json.body?.tenantName || 'Sozsoft';\nconst intent = content.includes('SQL') ? 'SQL' : 'CHAT';\nreturn [{ json: { question, tenantName, intent, intentRaw: res } }];"
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.code",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"position": [
|
||||||
|
-250,
|
||||||
|
200
|
||||||
|
],
|
||||||
|
"id": "adb78e6c-97b9-4a9a-8914-c2a4d935893e",
|
||||||
|
"name": "Normalize Intent"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"conditions": {
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true,
|
||||||
|
"leftValue": "",
|
||||||
|
"typeValidation": "strict",
|
||||||
|
"version": 2
|
||||||
|
},
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"id": "989bc03a-0479-4cee-a071-b2f991c5def5",
|
||||||
|
"leftValue": "={{ $json.intent }}",
|
||||||
|
"rightValue": "SQL",
|
||||||
|
"operator": {
|
||||||
|
"type": "string",
|
||||||
|
"operation": "equals"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"combinator": "and"
|
||||||
|
},
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.if",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"position": [
|
||||||
|
-40,
|
||||||
|
200
|
||||||
|
],
|
||||||
|
"id": "0221a195-1c08-47be-adab-1539dddce98b",
|
||||||
|
"name": "Is SQL?"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"jsCode": "const question = $('Webhook').first().json.body?.question || '';\nreturn [{ json: { type:'chat', question, ollamaBody: { model: 'qwen2.5:3b', stream: false, options: { temperature: 0.2, top_p: 0.8, num_predict: 512 }, messages: [ { role: 'system', content: `Sen Türkçe konuşan yardımcı bir asistansın. Kullanıcı SQL/veri istemiyorsa kısa ve net cevap ver. Veritabanı sorgusu üretme.` }, { role: 'user', content: question } ] } } }];"
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.code",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"position": [
|
||||||
|
180,
|
||||||
|
420
|
||||||
|
],
|
||||||
|
"id": "41617000-9033-4896-b04a-d2dfb48567ba",
|
||||||
|
"name": "Build Chat Request"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"method": "POST",
|
||||||
|
"url": "http://ollama:11434/api/chat",
|
||||||
|
"sendBody": true,
|
||||||
|
"specifyBody": "json",
|
||||||
|
"jsonBody": "={{ $json.ollamaBody }}",
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.httpRequest",
|
||||||
|
"typeVersion": 4.2,
|
||||||
|
"position": [
|
||||||
|
390,
|
||||||
|
420
|
||||||
|
],
|
||||||
|
"id": "b2f42e30-af31-447a-8e17-dc2e01bf2d96",
|
||||||
|
"name": "Chat Ollama"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"jsCode": "const res = $input.first().json || {};\nreturn [{ json: { type: 'chat', question: $('Webhook').first().json.body?.question || '', answer: res.message?.content || res.response || res.output || '' } }];"
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.code",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"position": [
|
||||||
|
610,
|
||||||
|
420
|
||||||
|
],
|
||||||
|
"id": "c9fae17b-b4d7-48df-8556-47ccc56556fb",
|
||||||
|
"name": "Chat Response"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"jsCode": "const dictionaryQuery = `\nSELECT\n STRING_AGG(\n CAST(\n N'Varlık: ' + ISNULL(EntityName, N'')\n + N' | Tablo: ' + ISNULL(TableName, N'')\n + N' | Kolon: ' + ISNULL(ColumnName, N'')\n + N' | Tip: ' + ISNULL(DataType, N'')\n + N' | Açıklama: ' + ISNULL(ColumnDescription, N'')\n + N' | Eş Anlamlılar: ' + ISNULL(Synonyms, N'')\n + CASE WHEN IsDefaultSelectColumn = 1 THEN N' | Varsayılan gösterilecek kolon' ELSE N'' END\n + CASE WHEN IsActiveColumn = 1 THEN N' | Aktiflik kolonu' ELSE N'' END\n + CASE WHEN IsDeletedColumn = 1 THEN N' | Silinmiş kayıt kolonu' ELSE N'' END\n + CASE WHEN IsSensitive = 1 THEN N' | Hassas alan' ELSE N'' END\n AS NVARCHAR(MAX)),\n CHAR(10)\n ) AS DataDictionaryPrompt\nFROM dbo.AI_DataDictionary;\n`;\nreturn [{ json: { question: $json.question, tenantName: $json.tenantName || 'Sozsoft', dictionaryQuery } }];"
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.code",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"position": [
|
||||||
|
180,
|
||||||
|
40
|
||||||
|
],
|
||||||
|
"id": "f92e673a-50ac-4551-8cff-a583f39e7ec3",
|
||||||
|
"name": "Build Data Dictionary Query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"operation": "executeQuery",
|
||||||
|
"query": "={{ $json.dictionaryQuery }}"
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.microsoftSql",
|
||||||
|
"typeVersion": 1.1,
|
||||||
|
"position": [
|
||||||
|
420,
|
||||||
|
40
|
||||||
|
],
|
||||||
|
"id": "61640dc9-12d9-4282-9277-b4760ac8f0e9",
|
||||||
|
"name": "Get Data Dictionary Prompt"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"jsCode": "const dict = String($input.first().json.DataDictionaryPrompt || '').trim();\nconst question = $('Webhook').first().json.body?.question || '';\nconst tenantName = $('Webhook').first().json.body?.tenantName || 'Sozsoft';\n\nconst tableNames = [...new Set([...dict.matchAll(/\\|\\s*Tablo:\\s*([^|\\n]+)/g)].map(m => m[1].trim()).filter(Boolean))];\nconst activeColumns = [...new Set([...dict.matchAll(/\\|\\s*Tablo:\\s*([^|\\n]+)\\s*\\|\\s*Kolon:\\s*([^|\\n]+)[^\\n]*Aktiflik kolonu/g)].map(m => `${m[1].trim()}.${m[2].trim()}`))];\nconst deletedColumns = [...new Set([...dict.matchAll(/\\|\\s*Tablo:\\s*([^|\\n]+)\\s*\\|\\s*Kolon:\\s*([^|\\n]+)[^\\n]*Silinmiş kayıt kolonu/g)].map(m => `${m[1].trim()}.${m[2].trim()}`))];\n\nconst userContent = `Kullanıcı isteği: ${question}\ntenantName: ${tenantName}\n\nVERI_SOZLUGU_CONTEXT:\n${dict || 'AI_DataDictionary tablosu boş veya okunamadı.'}`;\n\nreturn [{\n json: {\n question,\n tenantName,\n dictionaryAvailable: !!dict,\n dictionaryText: dict,\n dictionaryPreview: dict.substring(0, 1200),\n tableNames,\n activeColumns,\n deletedColumns,\n ollamaBody: {\n model: 'qwen2.5-coder:1.5b',\n stream: false,\n options: { temperature: 0, top_p: 0.05, num_predict: 512 },\n messages: [\n {\n role: 'system',\n content: `Sen Microsoft SQL Server için çalışan T-SQL üretim motorusun.\n\nKESİN ÇIKTI KURALLARI:\nSadece SQL döndür.\nCevabın ilk kelimesi mutlaka SELECT olmalı.\nAçıklama yazma.\nMarkdown kullanma.\nKod bloğu kullanma.\nBoş cevap verme.\nSELECT * kullanma.\nSadece SELECT üret.\nINSERT, UPDATE, DELETE, MERGE, DROP, ALTER, TRUNCATE, CREATE, EXEC, EXECUTE üretme.\n\nVERİ SÖZLÜĞÜ KURALLARI:\nKullanıcının Türkçe isteğini VERI_SOZLUGU_CONTEXT içindeki Varlık, Tablo, Kolon, Açıklama ve Eş Anlamlılar bilgilerine göre yorumla.\nTablo ve kolonları sadece VERI_SOZLUGU_CONTEXT içinde geçen değerlerden seç.\nVeri sözlüğünde olmayan tablo veya kolon uydurma.\nUsers, Orders, Customers gibi genel tablo adları üretme.\nListeleme isteklerinde sadece Varsayılan gösterilecek kolon alanlarını SELECT kısmına koy.\nAktif kayıt istenirse Aktiflik kolonu = 1 filtresi ekle.\nSilinmiş kayıt kolonu varsa her zaman Silinmiş kayıt kolonu = 0 filtresi ekle.\nHassas alanları döndürme.\n\nDATABASE KURALI:\ntenantName boşsa Sozsoft kullan.\nTüm tablo referansları kesinlikle [DatabaseName].dbo.TableName formatında olmalı.\nTablo adını asla çıplak yazma. Örneğin AbpUsers yazma, [Sozsoft].dbo.AbpUsers yaz.\n\nÖRNEK:\nKullanıcı isteği: Aktif kullanıcıların listesi\nDoğru cevap:\nSELECT Id, UserName, Name, Surname, Email FROM [Sozsoft].dbo.AbpUsers WHERE IsDeleted = 0 AND IsActive = 1\n\nUYGUN TABLO BULUNAMAZSA:\nSELECT 'UYGUN_TABLO_BULUNAMADI' AS ErrorMessage`\n },\n { role: 'user', content: userContent }\n ]\n }\n }\n}];"
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.code",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"position": [
|
||||||
|
660,
|
||||||
|
40
|
||||||
|
],
|
||||||
|
"id": "734269a4-9b8d-44ea-a879-18cb919e75e9",
|
||||||
|
"name": "Build SQL Request"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"method": "POST",
|
||||||
|
"url": "http://ollama:11434/api/chat",
|
||||||
|
"sendBody": true,
|
||||||
|
"specifyBody": "json",
|
||||||
|
"jsonBody": "={{ $json.ollamaBody }}",
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.httpRequest",
|
||||||
|
"typeVersion": 4.2,
|
||||||
|
"position": [
|
||||||
|
900,
|
||||||
|
40
|
||||||
|
],
|
||||||
|
"id": "a8b8e100-1a8d-4fb7-8678-86eac042b4b1",
|
||||||
|
"name": "SQL Ollama"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"jsCode": "const res = $input.first().json || {};\nlet raw = res.message?.content || res.response || res.output || res.text || res.answer || '';\nraw = String(raw || '').trim();\n\nconst question = $('Webhook').first().json.body?.question || '';\nconst tenantName = $('Webhook').first().json.body?.tenantName || 'Sozsoft';\nconst buildNode = $('Build SQL Request').first().json || {};\nconst tableNames = buildNode.tableNames || [];\nconst deletedColumns = buildNode.deletedColumns || [];\n\nfunction cleanGeneratedSql(text) {\n let t = String(text || '')\n .replace(/```sql/gi, '')\n .replace(/```/g, '')\n .replace(/^SQL\\s*:/i, '')\n .trim();\n\n const selectIndex = t.search(/\\bSELECT\\b/i);\n if (selectIndex < 0) return '';\n t = t.substring(selectIndex).trim();\n\n const secondSelectMatch = t.substring(6).search(/\\bSELECT\\b/i);\n if (secondSelectMatch >= 0) t = t.substring(0, 6 + secondSelectMatch).trim();\n\n const semicolonIndex = t.indexOf(';');\n if (semicolonIndex >= 0) t = t.substring(0, semicolonIndex).trim();\n\n const cutPatterns = [/\\n\\s*Açıklama\\s*:/i, /\\n\\s*Explanation\\s*:/i, /\\n\\s*Bu sorgu/i, /\\n\\s*Not\\s*:/i];\n for (const p of cutPatterns) {\n const m = t.search(p);\n if (m > 0) t = t.substring(0, m).trim();\n }\n return t.trim();\n}\n\nfunction escapeRegExp(s) {\n return String(s).replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n}\n\nfunction qualifyTables(sql) {\n let q = sql;\n const db = tenantName || 'Sozsoft';\n const uniqueTables = [...new Set(tableNames)].sort((a, b) => b.length - a.length);\n for (const table of uniqueTables) {\n const t = escapeRegExp(table);\n q = q.replace(new RegExp(`\\\\b(FROM|JOIN)\\\\s+(?!\\\\[)(?:dbo\\\\.)?(${t})\\\\b`, 'gi'), `$1 [${db}].dbo.$2`);\n }\n return q;\n}\n\nfunction addDefaultIsDeletedFilter(sql) {\n let q = sql.trim();\n for (const item of deletedColumns) {\n const [table, col] = String(item).split('.');\n if (!table || !col) continue;\n\n const tableRegex = new RegExp(`(\\\\[${escapeRegExp(tenantName)}\\\\]\\\\.dbo\\\\.|dbo\\\\.|\\\\b)${escapeRegExp(table)}\\\\b`, 'i');\n if (!tableRegex.test(q)) continue;\n\n const whereMatch = q.match(/\\bWHERE\\b([\\s\\S]*?)(\\bGROUP\\s+BY\\b|\\bORDER\\s+BY\\b|$)/i);\n const whereText = whereMatch ? whereMatch[1] : '';\n const colInWhere = new RegExp(`\\\\b${escapeRegExp(col)}\\\\b`, 'i').test(whereText);\n if (colInWhere) continue;\n\n const condition = `${col} = 0`;\n const orderGroupMatch = q.match(/\\b(GROUP\\s+BY|ORDER\\s+BY)\\b/i);\n if (/\\bWHERE\\b/i.test(q)) {\n if (orderGroupMatch) {\n const idx = orderGroupMatch.index;\n q = q.slice(0, idx).trim() + ` AND ${condition} ` + q.slice(idx).trim();\n } else {\n q = q + ` AND ${condition}`;\n }\n } else {\n if (orderGroupMatch) {\n const idx = orderGroupMatch.index;\n q = q.slice(0, idx).trim() + ` WHERE ${condition} ` + q.slice(idx).trim();\n } else {\n q = q + ` WHERE ${condition}`;\n }\n }\n }\n return q;\n}\n\nlet sql = cleanGeneratedSql(raw);\nsql = qualifyTables(sql);\nsql = addDefaultIsDeletedFilter(sql);\nsql = sql.replace(/;\\s*$/,'').trim();\n\nif (!sql) {\n return [{ json: { valid: false, type: 'sql_error', error: 'Üretilen çıktı içinde SELECT bulunamadı.', question, generatedText: raw, ollamaRawOutput: res, dictionaryAvailable: buildNode.dictionaryAvailable, dictionaryPreview: buildNode.dictionaryPreview } }];\n}\n\nif (!/^\\s*SELECT\\b/i.test(sql)) {\n return [{ json: { valid: false, type: 'sql_error', error: 'Üretilen çıktı SELECT ile başlamıyor.', question, generatedText: raw, cleanedText: sql, ollamaRawOutput: res, dictionaryAvailable: buildNode.dictionaryAvailable, dictionaryPreview: buildNode.dictionaryPreview } }];\n}\n\nconst forbidden = /\\b(INSERT|UPDATE|DELETE|MERGE|DROP|ALTER|TRUNCATE|CREATE|EXEC|EXECUTE|GRANT|REVOKE|BACKUP|RESTORE)\\b/i;\nif (forbidden.test(sql)) {\n return [{ json: { valid: false, type: 'sql_error', error: 'Güvenlik nedeniyle sadece SELECT sorgularına izin veriliyor.', question, generatedText: raw, cleanedText: sql } }];\n}\n\nconst withoutFirstSelect = sql.replace(/^\\s*SELECT\\b/i, '');\nif (/\\bSELECT\\b/i.test(withoutFirstSelect)) {\n return [{ json: { valid: false, type: 'sql_error', error: 'Tek seferde sadece bir SELECT sorgusu çalıştırılabilir.', question, generatedText: raw, cleanedText: sql } }];\n}\n\nreturn [{ json: { valid: true, type: 'sql', question, tenantName, sql, generatedText: raw } }];"
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.code",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"position": [
|
||||||
|
1130,
|
||||||
|
40
|
||||||
|
],
|
||||||
|
"id": "07579d4a-b1ef-4a8f-b8d6-f646af9c938f",
|
||||||
|
"name": "Validate SQL"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"conditions": {
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true,
|
||||||
|
"leftValue": "",
|
||||||
|
"typeValidation": "strict",
|
||||||
|
"version": 2
|
||||||
|
},
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"id": "f4c65cdb-f275-42a6-971c-ea1f62d9ff87",
|
||||||
|
"leftValue": "={{ $json.valid }}",
|
||||||
|
"rightValue": true,
|
||||||
|
"operator": {
|
||||||
|
"type": "boolean",
|
||||||
|
"operation": "true",
|
||||||
|
"singleValue": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"combinator": "and"
|
||||||
|
},
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.if",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"position": [
|
||||||
|
1350,
|
||||||
|
40
|
||||||
|
],
|
||||||
|
"id": "4f189011-b12c-465e-8de0-72c9bc1457ef",
|
||||||
|
"name": "SQL Valid?"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"operation": "executeQuery",
|
||||||
|
"query": "={{ $json.sql }}"
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.microsoftSql",
|
||||||
|
"typeVersion": 1.1,
|
||||||
|
"position": [
|
||||||
|
1580,
|
||||||
|
-60
|
||||||
|
],
|
||||||
|
"id": "a2ec036a-e23b-4904-89aa-766523f0dd35",
|
||||||
|
"name": "Execute Generated SQL"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"jsCode": "const rows = $input.all().map(i => i.json);\nconst validate = $('Validate SQL').first().json || {};\nconst columns = rows.length ? Object.keys(rows[0]) : [];\nreturn [{ json: { type: 'sql_result', question: validate.question, sql: validate.sql, columns, rows, rowCount: rows.length } }];"
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.code",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"position": [
|
||||||
|
1810,
|
||||||
|
-60
|
||||||
|
],
|
||||||
|
"id": "76a92057-357a-4acf-87f9-9bd52a55b85a",
|
||||||
|
"name": "Format SQL Result"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"jsCode": "const e = $input.first().json || {};\nreturn [{ json: { type: 'sql_error', question: e.question, error: e.error || 'SQL üretilemedi.', generatedText: e.generatedText || null, debug: { dictionaryAvailable: e.dictionaryAvailable, dictionaryPreview: e.dictionaryPreview, ollamaRawOutput: e.ollamaRawOutput || null } } }];"
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.code",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"position": [
|
||||||
|
1580,
|
||||||
|
160
|
||||||
|
],
|
||||||
|
"id": "c34f8a67-2b5c-4700-84fd-1171a4ce6485",
|
||||||
|
"name": "SQL Error Response"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"pinData": {},
|
"pinData": {},
|
||||||
|
|
@ -114,81 +294,179 @@
|
||||||
"main": [
|
"main": [
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"node": "Chat Memory Manager",
|
"node": "Build Intent Request",
|
||||||
"type": "main",
|
"type": "main",
|
||||||
"index": 0
|
"index": 0
|
||||||
}
|
}
|
||||||
],
|
]
|
||||||
[]
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"AI Chat": {
|
"Build Intent Request": {
|
||||||
"main": [
|
"main": [
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"node": "Chat Code",
|
"node": "Intent Ollama",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Intent Ollama": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Normalize Intent",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Normalize Intent": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Is SQL?",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Is SQL?": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Build Data Dictionary Query",
|
||||||
"type": "main",
|
"type": "main",
|
||||||
"index": 0
|
"index": 0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"node": "Chat Code",
|
"node": "Build Chat Request",
|
||||||
"type": "main",
|
"type": "main",
|
||||||
"index": 0
|
"index": 0
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"Chat Memory Manager": {
|
"Build Chat Request": {
|
||||||
"main": [
|
"main": [
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"node": "AI Chat",
|
"node": "Chat Ollama",
|
||||||
"type": "main",
|
"type": "main",
|
||||||
"index": 0
|
"index": 0
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"Simple Memory": {
|
"Chat Ollama": {
|
||||||
"ai_memory": [
|
"main": [
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"node": "AI Chat",
|
"node": "Chat Response",
|
||||||
"type": "ai_memory",
|
"type": "main",
|
||||||
"index": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"node": "Chat Memory Manager",
|
|
||||||
"type": "ai_memory",
|
|
||||||
"index": 0
|
"index": 0
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"Google Gemini Chat Model": {
|
"Build Data Dictionary Query": {
|
||||||
"ai_languageModel": [
|
"main": [
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"node": "AI Chat",
|
"node": "Get Data Dictionary Prompt",
|
||||||
"type": "ai_languageModel",
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Get Data Dictionary Prompt": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Build SQL Request",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Build SQL Request": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "SQL Ollama",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"SQL Ollama": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Validate SQL",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Validate SQL": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "SQL Valid?",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"SQL Valid?": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Execute Generated SQL",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "SQL Error Response",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Execute Generated SQL": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Format SQL Result",
|
||||||
|
"type": "main",
|
||||||
"index": 0
|
"index": 0
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"active": true,
|
"active": false,
|
||||||
"settings": {
|
"settings": {
|
||||||
"executionOrder": "v1",
|
"executionOrder": "v1"
|
||||||
"availableInMCP": false
|
|
||||||
},
|
},
|
||||||
"versionId": "d9036bcb-7257-49a0-b848-ff451bce5bb8",
|
"versionId": "074ecf8d-3021-4e36-b8cb-5fac6d27f68f",
|
||||||
"meta": {
|
"meta": {
|
||||||
"templateCredsSetupCompleted": true,
|
"templateCredsSetupCompleted": false
|
||||||
"instanceId": "1d288821beaaeeada5e8dce6f282c802098a0c83ef6ddb35a174b09a9d43850e"
|
|
||||||
},
|
},
|
||||||
"id": "R1V2XtEQCknLJvSkwUW6s",
|
"id": "ChatSQLDataDictionaryHTTPStableV2",
|
||||||
"tags": []
|
"tags": []
|
||||||
}
|
}
|
||||||
|
|
@ -10,6 +10,9 @@ networks:
|
||||||
external: false
|
external: false
|
||||||
ollama:
|
ollama:
|
||||||
external: false
|
external: false
|
||||||
|
db:
|
||||||
|
external: true
|
||||||
|
name: sozsoft-platform-data_db
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
rocket_mongodb_data:
|
rocket_mongodb_data:
|
||||||
|
|
@ -98,6 +101,7 @@ services:
|
||||||
- n8n_data:/home/node/.n8n
|
- n8n_data:/home/node/.n8n
|
||||||
networks:
|
networks:
|
||||||
- n8n
|
- n8n
|
||||||
|
- db
|
||||||
|
|
||||||
ollama:
|
ollama:
|
||||||
image: ollama/ollama:latest
|
image: ollama/ollama:latest
|
||||||
|
|
|
||||||
|
|
@ -8,13 +8,18 @@ export interface AiBotDto extends FullAuditedEntityDto<string> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
export type ChatType = 'chat' | 'query' | 'analyze'
|
export type ChatType = 'chat' | 'sql_result'
|
||||||
|
|
||||||
|
export type SqlResultRow = Record<string, unknown>
|
||||||
|
|
||||||
export interface BaseContent {
|
export interface BaseContent {
|
||||||
type: ChatType
|
type: ChatType
|
||||||
question: string
|
question: string
|
||||||
sql?: string | null
|
sql?: string | null
|
||||||
answer?: string | any[] | null
|
answer?: string | any[] | null
|
||||||
|
columns?: string[]
|
||||||
|
rows?: SqlResultRow[]
|
||||||
|
rowCount?: number
|
||||||
chart?: string
|
chart?: string
|
||||||
error?: string | null
|
error?: string | null
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,14 @@ import { useStoreState } from '@/store'
|
||||||
import { Avatar, Button, Drawer, Dropdown, ScrollBar } from '@/components/ui'
|
import { Avatar, Button, Drawer, Dropdown, ScrollBar } from '@/components/ui'
|
||||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||||
import { aiService } from '@/services/ai.service'
|
import { aiService } from '@/services/ai.service'
|
||||||
import { AiBotDto, BaseContent, Conversation, Message, MessageContent } from '@/proxy/ai/models'
|
import {
|
||||||
|
AiBotDto,
|
||||||
|
BaseContent,
|
||||||
|
Conversation,
|
||||||
|
Message,
|
||||||
|
MessageContent,
|
||||||
|
SqlResultRow,
|
||||||
|
} from '@/proxy/ai/models'
|
||||||
import { Container } from '@/components/shared'
|
import { Container } from '@/components/shared'
|
||||||
import { Helmet } from 'react-helmet'
|
import { Helmet } from 'react-helmet'
|
||||||
import { APP_NAME } from '@/constants/app.constant'
|
import { APP_NAME } from '@/constants/app.constant'
|
||||||
|
|
@ -80,7 +87,7 @@ const Assistant = () => {
|
||||||
const [selectedBot, setSelectedBot] = useState<string | null>(null)
|
const [selectedBot, setSelectedBot] = useState<string | null>(null)
|
||||||
|
|
||||||
const { user, tenant } = useStoreState((state) => state.auth)
|
const { user, tenant } = useStoreState((state) => state.auth)
|
||||||
const inputRef = useRef<HTMLTextAreaElement | null>(null)
|
const inputRef = useRef<HTMLInputElement | null>(null)
|
||||||
const bottomRef = useRef<HTMLDivElement | null>(null)
|
const bottomRef = useRef<HTMLDivElement | null>(null)
|
||||||
|
|
||||||
const { translate } = useLocalization()
|
const { translate } = useLocalization()
|
||||||
|
|
@ -305,6 +312,10 @@ const Assistant = () => {
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`AI bot request failed: ${response.status}`)
|
||||||
|
}
|
||||||
|
|
||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
const raw = Array.isArray(data) ? data[0] : data
|
const raw = Array.isArray(data) ? data[0] : data
|
||||||
|
|
||||||
|
|
@ -313,6 +324,9 @@ const Assistant = () => {
|
||||||
question: raw?.question || userMessage,
|
question: raw?.question || userMessage,
|
||||||
sql: raw?.sql ?? null,
|
sql: raw?.sql ?? null,
|
||||||
answer: raw?.answer ?? null,
|
answer: raw?.answer ?? null,
|
||||||
|
columns: Array.isArray(raw?.columns) ? raw.columns : undefined,
|
||||||
|
rows: Array.isArray(raw?.rows) ? raw.rows : undefined,
|
||||||
|
rowCount: typeof raw?.rowCount === 'number' ? raw.rowCount : undefined,
|
||||||
chart: raw?.chart,
|
chart: raw?.chart,
|
||||||
error: raw?.error ?? null,
|
error: raw?.error ?? null,
|
||||||
}
|
}
|
||||||
|
|
@ -341,20 +355,33 @@ const Assistant = () => {
|
||||||
// Mesaj içeriği render
|
// Mesaj içeriği render
|
||||||
const renderMessageContent = (message: Message) => {
|
const renderMessageContent = (message: Message) => {
|
||||||
if (message.role === 'assistant' && isContentObject(message.content)) {
|
if (message.role === 'assistant' && isContentObject(message.content)) {
|
||||||
const { type, sql, answer, chart, error } = message.content as BaseContent & {
|
const { type, question, sql, answer, chart, error, columns, rows, rowCount } =
|
||||||
|
message.content as BaseContent & {
|
||||||
error?: string
|
error?: string
|
||||||
}
|
}
|
||||||
|
const isSqlResult = type === 'sql_result'
|
||||||
|
const resultRows: SqlResultRow[] = isSqlResult
|
||||||
|
? Array.isArray(rows)
|
||||||
|
? rows
|
||||||
|
: []
|
||||||
|
: Array.isArray(answer)
|
||||||
|
? answer
|
||||||
|
: []
|
||||||
|
const resultColumns =
|
||||||
|
Array.isArray(columns) && columns.length > 0
|
||||||
|
? columns
|
||||||
|
: resultRows.length > 0
|
||||||
|
? Array.from(new Set(resultRows.flatMap((row) => Object.keys(row))))
|
||||||
|
: []
|
||||||
|
|
||||||
const typeStyles: Record<string, string> = {
|
const typeStyles: Record<string, string> = {
|
||||||
chat: 'bg-blue-100 text-blue-700 dark:bg-blue-900/50 dark:text-blue-200',
|
chat: 'bg-blue-100 text-blue-700 dark:bg-blue-900/50 dark:text-blue-200',
|
||||||
query: 'bg-green-100 text-green-700 dark:bg-green-900/50 dark:text-green-200',
|
sql_result: 'bg-gray-100 text-gray-700 dark:bg-gray-900/50 dark:text-gray-200',
|
||||||
analyze: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/50 dark:text-yellow-200',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const typeLabels: Record<string, string> = {
|
const typeLabels: Record<string, string> = {
|
||||||
chat: '🗨️ ' + translate('::AI.SohbetAnswer'),
|
chat: '🗨️ ' + translate('::AI.SohbetAnswer'),
|
||||||
query: '📊 ' + translate('::AI.DatabaseAnswer'),
|
sql_result: `📊 ${translate('::AI.DatabaseAnswer')}`,
|
||||||
analyze: '📈 ' + translate('::AI.AnalizAnswer'),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const cleanedSql = (() => {
|
const cleanedSql = (() => {
|
||||||
|
|
@ -367,14 +394,14 @@ const Assistant = () => {
|
||||||
})()
|
})()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-2 text-sm">
|
<div className="space-y-3 text-sm">
|
||||||
<div
|
<div
|
||||||
className={`inline-block px-3 py-1 text-xs font-medium rounded-full mb-2 ${typeStyles[type] || 'bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-200'}`}
|
className={`inline-block px-3 py-1 text-xs font-medium rounded-full mb-2 ${typeStyles[type] || 'bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-200'}`}
|
||||||
>
|
>
|
||||||
{typeLabels[type] || type}
|
{typeLabels[type] || type}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{sql && (
|
{sql && !isSqlResult && (
|
||||||
<div className="bg-gray-100 dark:bg-gray-800 p-3 rounded border border-gray-200 dark:border-gray-700 text-xs font-mono whitespace-pre-wrap">
|
<div className="bg-gray-100 dark:bg-gray-800 p-3 rounded border border-gray-200 dark:border-gray-700 text-xs font-mono whitespace-pre-wrap">
|
||||||
<pre>
|
<pre>
|
||||||
<code>{cleanedSql}</code>
|
<code>{cleanedSql}</code>
|
||||||
|
|
@ -389,7 +416,77 @@ const Assistant = () => {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!error &&
|
{!error &&
|
||||||
(typeof answer === 'string' ? (
|
(isSqlResult ? (
|
||||||
|
<div className="overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm dark:border-gray-900/70 dark:bg-gray-800">
|
||||||
|
{sql && (
|
||||||
|
<details className="group border-b border-gray-100 px-4 py-2.5 dark:border-gray-700">
|
||||||
|
<summary className="flex cursor-pointer list-none items-center justify-between gap-3 text-xs font-medium text-gray-500 marker:content-none hover:text-emerald-700 dark:text-gray-400 dark:hover:text-emerald-300">
|
||||||
|
<span>Oluşturulan SQL sorgusunu göster ({rowCount ?? resultRows.length} kayıt)</span>
|
||||||
|
<span className="text-base leading-none transition-transform group-open:rotate-45">
|
||||||
|
+
|
||||||
|
</span>
|
||||||
|
</summary>
|
||||||
|
<pre className="mt-3 max-h-48 overflow-auto rounded-lg bg-gray-950 p-3 text-xs leading-5 text-emerald-100">
|
||||||
|
<code>{cleanedSql}</code>
|
||||||
|
</pre>
|
||||||
|
</details>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{resultRows.length === 0 ? (
|
||||||
|
<div className="px-4 py-6 text-center text-gray-500 italic">
|
||||||
|
Sonuç bulunamadı.
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="max-h-[400px] overflow-auto">
|
||||||
|
<table className="w-full min-w-max text-sm">
|
||||||
|
<thead className="sticky top-0 z-10 bg-gray-800 text-xs uppercase tracking-wide text-gray-100 shadow-sm dark:bg-gray-950">
|
||||||
|
<tr>
|
||||||
|
{resultColumns.map((column) => (
|
||||||
|
<th
|
||||||
|
key={column}
|
||||||
|
className="whitespace-nowrap border-b border-r border-indigo-400/20 px-4 py-3 text-left font-semibold last:border-r-0"
|
||||||
|
>
|
||||||
|
{column}
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{resultRows.map((row, rowIndex) => (
|
||||||
|
<tr
|
||||||
|
key={rowIndex}
|
||||||
|
className="border-b border-gray-100 even:bg-slate-50/80 hover:bg-emerald-50/70 dark:border-gray-700 dark:even:bg-gray-800/60 dark:hover:bg-emerald-950/20"
|
||||||
|
>
|
||||||
|
{resultColumns.map((column) => {
|
||||||
|
const value = row[column]
|
||||||
|
const display =
|
||||||
|
value === null || value === undefined
|
||||||
|
? '—'
|
||||||
|
: typeof value === 'boolean'
|
||||||
|
? value
|
||||||
|
? 'Evet'
|
||||||
|
: 'Hayır'
|
||||||
|
: typeof value === 'object'
|
||||||
|
? JSON.stringify(value)
|
||||||
|
: String(value)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<td
|
||||||
|
key={column}
|
||||||
|
className="whitespace-nowrap border-r border-gray-100 px-2 py-2 align-top text-gray-700 first:font-medium first:text-slate-900 last:border-r-0 dark:border-gray-700 dark:text-gray-200 dark:first:text-white"
|
||||||
|
>
|
||||||
|
{display}
|
||||||
|
</td>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : typeof answer === 'string' ? (
|
||||||
renderFormattedText(answer)
|
renderFormattedText(answer)
|
||||||
) : Array.isArray(answer) ? (
|
) : Array.isArray(answer) ? (
|
||||||
answer.length === 0 ? (
|
answer.length === 0 ? (
|
||||||
|
|
@ -658,27 +755,28 @@ const Assistant = () => {
|
||||||
</div>
|
</div>
|
||||||
</ScrollBar>
|
</ScrollBar>
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} className="relative w-full mx-auto px-4">
|
<form onSubmit={handleSubmit} className="w-full mx-auto px-4">
|
||||||
<div className="relative w-full bg-[#2a2a2a] text-white rounded-3xl px-4 pt-4 pb-10 shadow-md">
|
<div className="flex w-full items-center gap-2 rounded-2xl bg-[#2a2a2a] px-3 py-2 text-white shadow-md">
|
||||||
<textarea
|
<input
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
value={input}
|
value={input}
|
||||||
autoFocus
|
autoFocus
|
||||||
onChange={(e) => setInput(e.target.value)}
|
onChange={(e) => setInput(e.target.value)}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === 'Enter' && !e.shiftKey) {
|
if (e.key === 'Enter') {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
handleSubmit(e)
|
handleSubmit(e)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
placeholder={translate('::AI.Asking')}
|
placeholder={translate('::AI.Asking')}
|
||||||
rows={2}
|
className="min-w-0 flex-1 bg-transparent px-1 text-base outline-none placeholder-gray-400"
|
||||||
className="w-full resize-none bg-transparent border-none outline-none text-base placeholder-gray-400"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="absolute bottom-1 left-2 flex items-center gap-2 text-sm">
|
<div className="shrink-0 text-sm">
|
||||||
<Dropdown
|
<Dropdown
|
||||||
placement="top-start"
|
placement="top-end"
|
||||||
|
menuClass="w-48 max-w-[calc(100vw-2rem)]"
|
||||||
|
toggleClassName="[&_svg]:ml-2"
|
||||||
title={
|
title={
|
||||||
bot.find((item) => item.id === selectedBot)?.name ||
|
bot.find((item) => item.id === selectedBot)?.name ||
|
||||||
translate('::AI.SelectModel')
|
translate('::AI.SelectModel')
|
||||||
|
|
@ -694,17 +792,16 @@ const Assistant = () => {
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="absolute bottom-2 right-3 flex items-center gap-2">
|
<div className="ml-2 flex shrink-0 items-center border-l border-white/15 pl-3">
|
||||||
{/* <button
|
<Button
|
||||||
type="button"
|
size="sm"
|
||||||
aria-label="Mic"
|
shape="circle"
|
||||||
className="rounded-full h-8 w-8 flex items-center justify-center text-gray-400 hover:text-white"
|
variant="plain"
|
||||||
>
|
type="submit"
|
||||||
🎤
|
icon={<FaPaperPlane />}
|
||||||
</button> */}
|
disabled={loading}
|
||||||
<Button size="sm" variant="plain" type="submit" disabled={loading}>
|
className="bg-white !text-gray-800 shadow-sm hover:bg-gray-100"
|
||||||
<FaPaperPlane />
|
/>
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue