From f55e3a7e1bc9e308c84ba9cf1b87cfa78be762e4 Mon Sep 17 00:00:00 2001 From: tegnike Date: Tue, 3 Dec 2024 22:59:27 +0100 Subject: [PATCH 1/5] =?UTF-8?q?=E4=BC=9A=E8=A9=B1=E5=B1=A5=E6=AD=B4?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=E6=99=82=E3=81=ABSupabase=E3=81=AB=E3=83=87?= =?UTF-8?q?=E3=83=BC=E3=82=BF=E3=82=92=E4=BF=9D=E5=AD=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 194 +++++++++++++++++++++++++++++++++ package.json | 1 + src/pages/api/save-chat-log.ts | 66 ++++++++++- 3 files changed, 260 insertions(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index ce989b3..bd0fd12 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,7 @@ "@marp-team/marp-core": "^3.9.0", "@marp-team/marpit": "^3.0.0", "@pixiv/three-vrm": "^3.0.0", + "@supabase/supabase-js": "^2.46.2", "@tailwindcss/line-clamp": "^0.4.4", "@types/uuid": "^10.0.0", "@vercel/analytics": "^1.3.1", @@ -1967,6 +1968,80 @@ "url": "https://github.com/sindresorhus/is?sponsor=1" } }, + "node_modules/@supabase/auth-js": { + "version": "2.65.1", + "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.65.1.tgz", + "integrity": "sha512-IA7i2Xq2SWNCNMKxwmPlHafBQda0qtnFr8QnyyBr+KaSxoXXqEzFCnQ1dGTy6bsZjVBgXu++o3qrDypTspaAPw==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "^2.6.14" + } + }, + "node_modules/@supabase/functions-js": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.4.3.tgz", + "integrity": "sha512-sOLXy+mWRyu4LLv1onYydq+10mNRQ4rzqQxNhbrKLTLTcdcmS9hbWif0bGz/NavmiQfPs4ZcmQJp4WqOXlR4AQ==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "^2.6.14" + } + }, + "node_modules/@supabase/node-fetch": { + "version": "2.6.15", + "resolved": "https://registry.npmjs.org/@supabase/node-fetch/-/node-fetch-2.6.15.tgz", + "integrity": "sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + } + }, + "node_modules/@supabase/postgrest-js": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-1.16.3.tgz", + "integrity": "sha512-HI6dsbW68AKlOPofUjDTaosiDBCtW4XAm0D18pPwxoW3zKOE2Ru13Z69Wuys9fd6iTpfDViNco5sgrtnP0666A==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "^2.6.14" + } + }, + "node_modules/@supabase/realtime-js": { + "version": "2.10.9", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.10.9.tgz", + "integrity": "sha512-0AjN65VDNIScZzrrPaVvlND4vbgVS+j9Wcy3zf7e+l9JY4IwCTahFenPLcKy9bkr7KY0wfB7MkipZPKxMaDnjw==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "^2.6.14", + "@types/phoenix": "^1.5.4", + "@types/ws": "^8.5.10", + "ws": "^8.18.0" + } + }, + "node_modules/@supabase/storage-js": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.7.1.tgz", + "integrity": "sha512-asYHcyDR1fKqrMpytAS1zjyEfvxuOIp1CIXX7ji4lHHcJKqyk+sLl/Vxgm4sN6u8zvuUtae9e4kDxQP2qrwWBA==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "^2.6.14" + } + }, + "node_modules/@supabase/supabase-js": { + "version": "2.46.2", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.46.2.tgz", + "integrity": "sha512-5FEzYMZhfIZrMWEqo5/dQincvrhM+DeMWH3/okeZrkBBW1AJxblOQhnhF4/dfNYK25oZ1O8dAnnxZ9gQqdr40w==", + "license": "MIT", + "dependencies": { + "@supabase/auth-js": "2.65.1", + "@supabase/functions-js": "2.4.3", + "@supabase/node-fetch": "2.6.15", + "@supabase/postgrest-js": "1.16.3", + "@supabase/realtime-js": "2.10.9", + "@supabase/storage-js": "2.7.1" + } + }, "node_modules/@swc/counter": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", @@ -2135,6 +2210,12 @@ "form-data": "^4.0.0" } }, + "node_modules/@types/phoenix": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.6.tgz", + "integrity": "sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==", + "license": "MIT" + }, "node_modules/@types/prop-types": { "version": "15.7.12", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", @@ -2229,6 +2310,15 @@ "integrity": "sha512-4hxA+NwohSgImdTSlPXEqDqqFktNgmTXQ05ff1uWam05tNGroCMp4G+4XVl6qWm1p7GQ/9oD41kAYsSssF6Mzw==", "dev": true }, + "node_modules/@types/ws": { + "version": "8.5.13", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.13.tgz", + "integrity": "sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/yauzl": { "version": "2.10.3", "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", @@ -10427,6 +10517,27 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/xmldom-sre": { "version": "0.1.31", "resolved": "https://registry.npmjs.org/xmldom-sre/-/xmldom-sre-0.1.31.tgz", @@ -11884,6 +11995,70 @@ "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", "dev": true }, + "@supabase/auth-js": { + "version": "2.65.1", + "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.65.1.tgz", + "integrity": "sha512-IA7i2Xq2SWNCNMKxwmPlHafBQda0qtnFr8QnyyBr+KaSxoXXqEzFCnQ1dGTy6bsZjVBgXu++o3qrDypTspaAPw==", + "requires": { + "@supabase/node-fetch": "^2.6.14" + } + }, + "@supabase/functions-js": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.4.3.tgz", + "integrity": "sha512-sOLXy+mWRyu4LLv1onYydq+10mNRQ4rzqQxNhbrKLTLTcdcmS9hbWif0bGz/NavmiQfPs4ZcmQJp4WqOXlR4AQ==", + "requires": { + "@supabase/node-fetch": "^2.6.14" + } + }, + "@supabase/node-fetch": { + "version": "2.6.15", + "resolved": "https://registry.npmjs.org/@supabase/node-fetch/-/node-fetch-2.6.15.tgz", + "integrity": "sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ==", + "requires": { + "whatwg-url": "^5.0.0" + } + }, + "@supabase/postgrest-js": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-1.16.3.tgz", + "integrity": "sha512-HI6dsbW68AKlOPofUjDTaosiDBCtW4XAm0D18pPwxoW3zKOE2Ru13Z69Wuys9fd6iTpfDViNco5sgrtnP0666A==", + "requires": { + "@supabase/node-fetch": "^2.6.14" + } + }, + "@supabase/realtime-js": { + "version": "2.10.9", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.10.9.tgz", + "integrity": "sha512-0AjN65VDNIScZzrrPaVvlND4vbgVS+j9Wcy3zf7e+l9JY4IwCTahFenPLcKy9bkr7KY0wfB7MkipZPKxMaDnjw==", + "requires": { + "@supabase/node-fetch": "^2.6.14", + "@types/phoenix": "^1.5.4", + "@types/ws": "^8.5.10", + "ws": "^8.18.0" + } + }, + "@supabase/storage-js": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.7.1.tgz", + "integrity": "sha512-asYHcyDR1fKqrMpytAS1zjyEfvxuOIp1CIXX7ji4lHHcJKqyk+sLl/Vxgm4sN6u8zvuUtae9e4kDxQP2qrwWBA==", + "requires": { + "@supabase/node-fetch": "^2.6.14" + } + }, + "@supabase/supabase-js": { + "version": "2.46.2", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.46.2.tgz", + "integrity": "sha512-5FEzYMZhfIZrMWEqo5/dQincvrhM+DeMWH3/okeZrkBBW1AJxblOQhnhF4/dfNYK25oZ1O8dAnnxZ9gQqdr40w==", + "requires": { + "@supabase/auth-js": "2.65.1", + "@supabase/functions-js": "2.4.3", + "@supabase/node-fetch": "2.6.15", + "@supabase/postgrest-js": "1.16.3", + "@supabase/realtime-js": "2.10.9", + "@supabase/storage-js": "2.7.1" + } + }, "@swc/counter": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", @@ -12029,6 +12204,11 @@ "form-data": "^4.0.0" } }, + "@types/phoenix": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.6.tgz", + "integrity": "sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==" + }, "@types/prop-types": { "version": "15.7.12", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", @@ -12121,6 +12301,14 @@ "integrity": "sha512-4hxA+NwohSgImdTSlPXEqDqqFktNgmTXQ05ff1uWam05tNGroCMp4G+4XVl6qWm1p7GQ/9oD41kAYsSssF6Mzw==", "dev": true }, + "@types/ws": { + "version": "8.5.13", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.13.tgz", + "integrity": "sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA==", + "requires": { + "@types/node": "*" + } + }, "@types/yauzl": { "version": "2.10.3", "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", @@ -17983,6 +18171,12 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, + "ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "requires": {} + }, "xmldom-sre": { "version": "0.1.31", "resolved": "https://registry.npmjs.org/xmldom-sre/-/xmldom-sre-0.1.31.tgz", diff --git a/package.json b/package.json index d677aa6..5bdb043 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "@marp-team/marp-core": "^3.9.0", "@marp-team/marpit": "^3.0.0", "@pixiv/three-vrm": "^3.0.0", + "@supabase/supabase-js": "^2.46.2", "@tailwindcss/line-clamp": "^0.4.4", "@types/uuid": "^10.0.0", "@vercel/analytics": "^1.3.1", diff --git a/src/pages/api/save-chat-log.ts b/src/pages/api/save-chat-log.ts index 3dfe724..ff8afcb 100644 --- a/src/pages/api/save-chat-log.ts +++ b/src/pages/api/save-chat-log.ts @@ -1,7 +1,17 @@ +import { createClient, SupabaseClient } from '@supabase/supabase-js' import { NextApiRequest, NextApiResponse } from 'next' import fs from 'fs' import path from 'path' +// Supabaseクライアントの初期化 +let supabase: SupabaseClient | null = null +if (process.env.SUPABASE_URL && process.env.SUPABASE_SERVICE_ROLE_KEY) { + supabase = createClient( + process.env.SUPABASE_URL!, + process.env.SUPABASE_SERVICE_ROLE_KEY! + ) +} + export default async function handler( req: NextApiRequest, res: NextApiResponse @@ -12,6 +22,7 @@ export default async function handler( try { const { messages, isNewFile } = req.body + const created_at = new Date().toISOString() // メッセージ内の画像データを省略 const processedMessages = messages.map((msg: any) => { @@ -40,12 +51,65 @@ export default async function handler( } const fileName = isNewFile - ? `log_${new Date().toISOString().replace(/[:.]/g, '-')}.json` + ? `log_${created_at.replace(/[:.]/g, '-')}.json` : getLatestLogFile(logsDir) const filePath = path.join(logsDir, fileName) fs.writeFileSync(filePath, JSON.stringify(processedMessages, null, 2)) + if (supabase) { + // 既存のセッションを検索 + const { data: existingSession } = await supabase + .from('local_chat_sessions') + .select() + .eq('title', fileName) + .single() + + let sessionId + + if (existingSession) { + // 既存のセッションが見つかった場合 + sessionId = existingSession.id + + // updated_at のみ更新 + await supabase + .from('local_chat_sessions') + .update({ updated_at: created_at }) + .eq('id', sessionId) + } else { + // 新しいセッションを作成 + const { data: newSession, error: sessionError } = await supabase + .from('local_chat_sessions') + .insert({ + title: fileName, + created_at: created_at, + updated_at: created_at, + }) + .select() + .single() + + if (sessionError) throw sessionError + sessionId = newSession.id + } + + // 最新のメッセージのみを保存 + const lastMessage = processedMessages[processedMessages.length - 1] + const messageToSave = { + session_id: sessionId, + role: lastMessage.role, + content: Array.isArray(lastMessage.content) + ? JSON.stringify(lastMessage.content) + : lastMessage.content, + created_at: created_at, + } + + const { error: messageError } = await supabase + .from('local_messages') + .insert(messageToSave) + + if (messageError) throw messageError + } + res.status(200).json({ message: 'Log saved successfully' }) } catch (error) { console.error('Error saving chat log:', error) From 6c1789729a790fcf649f67724290abdda265a082 Mon Sep 17 00:00:00 2001 From: tegnike Date: Wed, 4 Dec 2024 12:41:28 +0100 Subject: [PATCH 2/5] =?UTF-8?q?=E3=83=AD=E3=83=BC=E3=82=AB=E3=83=ABLLM?= =?UTF-8?q?=E3=82=92API=E7=B5=8C=E7=94=B1=E3=81=A7=E5=AE=9F=E8=A1=8C=20&?= =?UTF-8?q?=20LM=20Studio=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/settings/modelProvider.tsx | 2 + src/features/chat/localLLMChat.ts | 42 +++++++-------- src/pages/api/local-llm.ts | 62 +++++++++++++++++++++++ tsconfig.json | 9 +++- 4 files changed, 93 insertions(+), 22 deletions(-) create mode 100644 src/pages/api/local-llm.ts diff --git a/src/components/settings/modelProvider.tsx b/src/components/settings/modelProvider.tsx index ca0bcd5..0465c67 100644 --- a/src/components/settings/modelProvider.tsx +++ b/src/components/settings/modelProvider.tsx @@ -828,6 +828,8 @@ const ModelProvider = () => { {t('LocalLLMInfo2')}
ex. Ollama: http://localhost:11434/v1/chat/completions +
+ ex. LM Studio: http://localhost:1234/v1/chat/completions
{t('EnterURL')} diff --git a/src/features/chat/localLLMChat.ts b/src/features/chat/localLLMChat.ts index 4eb4ddb..699fb78 100644 --- a/src/features/chat/localLLMChat.ts +++ b/src/features/chat/localLLMChat.ts @@ -1,4 +1,3 @@ -import axios from 'axios' import { Message } from '../messages/messages' export async function getLocalLLMChatResponseStream( @@ -6,41 +5,44 @@ export async function getLocalLLMChatResponseStream( localLlmUrl: string, model?: string ) { - const response = await axios.post( - localLlmUrl.replace(/\/$/, ''), - { - model: model, - messages: messages, - stream: true, - }, - { - responseType: 'stream', - } - ) + const response = await fetch('/api/local-llm', { + method: 'POST', + body: JSON.stringify({ + localLlmUrl, + model, + messages, + }), + }) + + const stream = response.body + if (!stream) { + throw new Error('No stream in response') + } - const stream = response.data + const reader = stream.getReader() const res = new ReadableStream({ async start(controller: ReadableStreamDefaultController) { let accumulatedChunks = '' try { - for await (const chunk of stream) { + while (true) { + const { done, value } = await reader.read() + if (done) break + + const chunk = new TextDecoder().decode(value) accumulatedChunks += chunk - // console.log(accumulatedChunks); + try { - // 累積されたチャンクを解析 const trimmedChunks = accumulatedChunks.trimStart() const data = JSON.parse(trimmedChunks.slice(6)) - // JSONが正常に解析された場合、必要なデータを抽出 if (data.choices && data.choices.length > 0) { const content = data.choices[0].delta.content controller.enqueue(content) - accumulatedChunks = '' // JSONが成功したのでチャンクをリセット + accumulatedChunks = '' } } catch (error) { - // console.log("accumulatedChunks: `" + accumulatedChunks + "`"); - // JSONが不完全であるため、さらにチャンクを累積 + // JSONが不完全な場合は続行 } } } catch (error) { diff --git a/src/pages/api/local-llm.ts b/src/pages/api/local-llm.ts new file mode 100644 index 0000000..b4b3eaa --- /dev/null +++ b/src/pages/api/local-llm.ts @@ -0,0 +1,62 @@ +import type { NextApiRequest, NextApiResponse } from 'next' +import axios from 'axios' + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + if (req.method !== 'POST') { + return res.status(405).json({ error: 'Method not allowed' }) + } + + const body = typeof req.body === 'string' ? JSON.parse(req.body) : req.body + + console.log('Parsed body:', body) + console.log('localLlmUrl:', body.localLlmUrl) + + if (!body.localLlmUrl) { + return res.status(400).json({ + error: 'localLlmUrl is required', + receivedBody: body, + }) + } + + try { + const response = await axios.post( + body.localLlmUrl.replace(/\/$/, ''), + { + model: body.model, + messages: body.messages, + stream: true, + }, + { + headers: { + 'Content-Type': 'application/json', + }, + responseType: 'stream', + } + ) + + response.data.pipe(res) + } catch (error) { + console.error('Error details:', error) + + let errorMessage = 'Error processing request' + + if (error && typeof error === 'object') { + const err = error as any + if ( + 'response' in err && + err.response && + typeof err.response === 'object' && + 'data' in err.response + ) { + errorMessage = err.response.data + } else if ('message' in err && typeof err.message === 'string') { + errorMessage = err.message + } + } + + res.status(500).json({ error: errorMessage }) + } +} diff --git a/tsconfig.json b/tsconfig.json index a505220..224592c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,8 +16,13 @@ "incremental": true, "paths": { "@/*": ["./src/*"] - } + }, + "plugins": [ + { + "name": "next" + } + ] }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], "exclude": ["node_modules"] } From 43509b0d920d5c80eae8e9f6550ffce5bceb784d Mon Sep 17 00:00:00 2001 From: tegnike Date: Fri, 6 Dec 2024 23:41:14 +0100 Subject: [PATCH 3/5] =?UTF-8?q?=E3=82=A8=E3=83=A9=E3=83=BC=E3=83=8F?= =?UTF-8?q?=E3=83=B3=E3=83=89=E3=83=AA=E3=83=B3=E3=82=B0=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- locales/en/translation.json | 7 +- locales/ja/translation.json | 7 +- locales/ko/translation.json | 7 +- locales/zh/translation.json | 7 +- src/features/chat/localLLMChat.ts | 122 ++++++++++++++++++++---------- src/pages/api/local-llm.ts | 27 ++++--- 6 files changed, 120 insertions(+), 57 deletions(-) diff --git a/locales/en/translation.json b/locales/en/translation.json index dad973f..0589124 100644 --- a/locales/en/translation.json +++ b/locales/en/translation.json @@ -143,7 +143,12 @@ "InvalidAIService": "The selected AI service is not valid", "MethodNotAllowed": "The request is not appropriate", "TTSServiceError": "An error occurred in the {{serviceName}} TTS service: {{message}}", - "UnexpectedError": "An unexpected error occurred" + "UnexpectedError": "An unexpected error occurred", + "LocalLLMError": "Local LLM error", + "LocalLLMStreamError": "Local LLM stream error", + "LocalLLMConnectionError": "Local LLM server connection error", + "LocalLLMNotFound": "Local LLM endpoint not found", + "LocalLLMAPIError": "Local LLM API error" }, "MessageReceiver": "Receive instructions from outside", "MessageReceiverDescription": "You can use API to instruct AI characters to speak from outside.", diff --git a/locales/ja/translation.json b/locales/ja/translation.json index 4c159da..7c4bd03 100644 --- a/locales/ja/translation.json +++ b/locales/ja/translation.json @@ -144,7 +144,12 @@ "InvalidAIService": "選択しているAIサービスが正しくありません", "MethodNotAllowed": "リクエストが適切でありません", "TTSServiceError": "{{serviceName}} TTSサービスでエラーが発生しました: {{message}}", - "UnexpectedError": "不明なエラーが発生しました" + "UnexpectedError": "不明なエラーが発生しました", + "LocalLLMError": "ローカルLLMでエラーが発生しました", + "LocalLLMStreamError": "ローカルLLMのストリーム処理でエラーが発生しました", + "LocalLLMConnectionError": "ローカルLLMサーバーに接続できません", + "LocalLLMNotFound": "ローカルLLMのエンドポイントが見つかりません", + "LocalLLMAPIError": "ローカルLLM APIでエラーが発生しました" }, "MessageReceiver": "外部からの指示を受け付ける", "MessageReceiverDescription": "APIを利用してAIキャラの発言を外部から指示することができます。", diff --git a/locales/ko/translation.json b/locales/ko/translation.json index 6c523bf..0c8a681 100644 --- a/locales/ko/translation.json +++ b/locales/ko/translation.json @@ -143,7 +143,12 @@ "InvalidAIService": "선택한 AI 서비스가 올바르지 않습니다", "MethodNotAllowed": "요청이 적절하지 않습니다", "TTSServiceError": "{{serviceName}} TTS 서비스에서 오류가 발생했습니다: {{message}}", - "UnexpectedError": "예기치 않은 오류가 발생했습니다" + "UnexpectedError": "예기치 않은 오류가 발생했습니다", + "LocalLLMError": "로컬 LLM에서 오류가 발생했습니다", + "LocalLLMStreamError": "로컬 LLM의 스트림 처리에서 오류가 발생했습니다", + "LocalLLMConnectionError": "로컬 LLM 서버에 연결할 수 없습니다", + "LocalLLMNotFound": "로컬 LLM의 엔드포인트를 찾을 수 없습니다", + "LocalLLMAPIError": "로컬 LLM API에서 오류가 발생했습니다" }, "MessageReceiver": "외부에서 지시를 받는다", "MessageReceiverDescription": "API를 사용하여 AI 캐릭터의 말을 외부에서 지시할 수 있습니다.", diff --git a/locales/zh/translation.json b/locales/zh/translation.json index 0c32fa9..f4024c6 100644 --- a/locales/zh/translation.json +++ b/locales/zh/translation.json @@ -143,7 +143,12 @@ "InvalidAIService": "選擇的 AI 服務不正確", "MethodNotAllowed": "請求不適當", "TTSServiceError": "{{serviceName}} TTS服務發生錯誤:{{message}}", - "UnexpectedError": "發生了意外錯誤" + "UnexpectedError": "發生了意外錯誤", + "LocalLLMError": "Local LLM 發生錯誤", + "LocalLLMStreamError": "Local LLM 的串流處理發生錯誤", + "LocalLLMConnectionError": "Local LLM 伺服器無法連線", + "LocalLLMNotFound": "Local LLM 的端點無法找到", + "LocalLLMAPIError": "Local LLM API 發生錯誤" }, "MessageReceiver": "接收外部指示", "MessageReceiverDescription": "你可以使用 API 來指示 AI 角色從外部說話。", diff --git a/src/features/chat/localLLMChat.ts b/src/features/chat/localLLMChat.ts index 699fb78..710cc88 100644 --- a/src/features/chat/localLLMChat.ts +++ b/src/features/chat/localLLMChat.ts @@ -1,57 +1,97 @@ import { Message } from '../messages/messages' +import i18next from 'i18next' +import toastStore from '@/features/stores/toast' +import settingsStore from '@/features/stores/settings' + +function handleApiError(errorCode: string): string { + const languageCode = settingsStore.getState().selectLanguage + i18next.changeLanguage(languageCode) + return i18next.t(`Errors.${errorCode || 'LocalLLMError'}`) +} export async function getLocalLLMChatResponseStream( messages: Message[], localLlmUrl: string, model?: string ) { - const response = await fetch('/api/local-llm', { - method: 'POST', - body: JSON.stringify({ - localLlmUrl, - model, - messages, - }), - }) - - const stream = response.body - if (!stream) { - throw new Error('No stream in response') - } + try { + const response = await fetch('/api/local-llm', { + method: 'POST', + body: JSON.stringify({ + localLlmUrl, + model, + messages, + }), + }) - const reader = stream.getReader() + if (!response.ok) { + const responseBody = await response.json() + throw new Error( + `Local LLM API request failed with status ${response.status}`, + { cause: { errorCode: responseBody.errorCode } } + ) + } - const res = new ReadableStream({ - async start(controller: ReadableStreamDefaultController) { - let accumulatedChunks = '' - try { - while (true) { - const { done, value } = await reader.read() - if (done) break + const stream = response.body + if (!stream) { + throw new Error('No stream in response', { + cause: { errorCode: 'LocalLLMStreamError' }, + }) + } - const chunk = new TextDecoder().decode(value) - accumulatedChunks += chunk + const reader = stream.getReader() - try { - const trimmedChunks = accumulatedChunks.trimStart() - const data = JSON.parse(trimmedChunks.slice(6)) + return new ReadableStream({ + async start(controller: ReadableStreamDefaultController) { + let accumulatedChunks = '' + try { + while (true) { + const { done, value } = await reader.read() + if (done) break - if (data.choices && data.choices.length > 0) { - const content = data.choices[0].delta.content - controller.enqueue(content) - accumulatedChunks = '' + const chunk = new TextDecoder().decode(value) + accumulatedChunks += chunk + + try { + const trimmedChunks = accumulatedChunks.trimStart() + const data = JSON.parse(trimmedChunks.slice(6)) + + if (data.choices && data.choices.length > 0) { + const content = data.choices[0].delta.content + controller.enqueue(content) + accumulatedChunks = '' + } + } catch (error) { + // JSONが不完全な場合は続行 } - } catch (error) { - // JSONが不完全な場合は続行 } + } catch (error: any) { + console.error('Error in Local LLM stream:', error) + const errorMessage = handleApiError( + error.cause?.errorCode || 'LocalLLMStreamError' + ) + toastStore.getState().addToast({ + message: errorMessage, + type: 'error', + tag: 'local-llm-error', + }) + controller.error(error) + } finally { + controller.close() + reader.releaseLock() } - } catch (error) { - controller.error(error) - } finally { - controller.close() - } - }, - }) - - return res + }, + }) + } catch (error: any) { + console.error('Error in Local LLM request:', error) + const errorMessage = handleApiError( + error.cause?.errorCode || 'LocalLLMError' + ) + toastStore.getState().addToast({ + message: errorMessage, + type: 'error', + tag: 'local-llm-error', + }) + throw error + } } diff --git a/src/pages/api/local-llm.ts b/src/pages/api/local-llm.ts index b4b3eaa..6a80acb 100644 --- a/src/pages/api/local-llm.ts +++ b/src/pages/api/local-llm.ts @@ -11,9 +11,6 @@ export default async function handler( const body = typeof req.body === 'string' ? JSON.parse(req.body) : req.body - console.log('Parsed body:', body) - console.log('localLlmUrl:', body.localLlmUrl) - if (!body.localLlmUrl) { return res.status(400).json({ error: 'localLlmUrl is required', @@ -39,24 +36,30 @@ export default async function handler( response.data.pipe(res) } catch (error) { - console.error('Error details:', error) + console.error('Error in Local LLM API call:', error) let errorMessage = 'Error processing request' + let errorCode = 'LocalLLMError' if (error && typeof error === 'object') { const err = error as any - if ( - 'response' in err && - err.response && - typeof err.response === 'object' && - 'data' in err.response - ) { + if (err.code === 'ECONNREFUSED') { + errorMessage = 'Failed to connect to Local LLM server' + errorCode = 'LocalLLMConnectionError' + } else if (err.response?.status === 404) { + errorMessage = 'Local LLM endpoint not found' + errorCode = 'LocalLLMNotFound' + } else if (err.response?.data) { errorMessage = err.response.data - } else if ('message' in err && typeof err.message === 'string') { + errorCode = 'LocalLLMAPIError' + } else if (err.message) { errorMessage = err.message } } - res.status(500).json({ error: errorMessage }) + res.status(500).json({ + error: errorMessage, + errorCode: errorCode, + }) } } From b908f35b8009188e7bc472216513e9e366031c3b Mon Sep 17 00:00:00 2001 From: tegnike Date: Fri, 6 Dec 2024 23:55:05 +0100 Subject: [PATCH 4/5] =?UTF-8?q?=E3=82=B3=E3=83=A1=E3=83=B3=E3=83=88?= =?UTF-8?q?=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/api/save-chat-log.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/api/save-chat-log.ts b/src/pages/api/save-chat-log.ts index ff8afcb..2fa3db8 100644 --- a/src/pages/api/save-chat-log.ts +++ b/src/pages/api/save-chat-log.ts @@ -57,6 +57,7 @@ export default async function handler( const filePath = path.join(logsDir, fileName) fs.writeFileSync(filePath, JSON.stringify(processedMessages, null, 2)) + // TODO: 標準化する if (supabase) { // 既存のセッションを検索 const { data: existingSession } = await supabase From cbfcc073ffdec73f4d04208ce4a9a15bdc782508 Mon Sep 17 00:00:00 2001 From: tegnike Date: Fri, 6 Dec 2024 23:59:03 +0100 Subject: [PATCH 5/5] =?UTF-8?q?=E3=83=90=E3=83=BC=E3=82=B8=E3=83=A7?= =?UTF-8?q?=E3=83=B3=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/settings/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/settings/index.tsx b/src/components/settings/index.tsx index 4079a26..ce0645e 100644 --- a/src/components/settings/index.tsx +++ b/src/components/settings/index.tsx @@ -119,7 +119,7 @@ const Main = () => { const Footer = () => { return ( ) }