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/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/components/settings/index.tsx b/src/components/settings/index.tsx index 976ad33..ce0645e 100644 --- a/src/components/settings/index.tsx +++ b/src/components/settings/index.tsx @@ -119,7 +119,7 @@ const Main = () => { const Footer = () => { return ( ) } 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..710cc88 100644 --- a/src/features/chat/localLLMChat.ts +++ b/src/features/chat/localLLMChat.ts @@ -1,55 +1,97 @@ -import axios from 'axios' 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 axios.post( - localLlmUrl.replace(/\/$/, ''), - { - model: model, - messages: messages, - stream: true, - }, - { - responseType: 'stream', + try { + const response = await fetch('/api/local-llm', { + method: 'POST', + body: JSON.stringify({ + localLlmUrl, + model, + messages, + }), + }) + + 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 stream = response.body + if (!stream) { + throw new Error('No stream in response', { + cause: { errorCode: 'LocalLLMStreamError' }, + }) } - ) - - const stream = response.data - - const res = new ReadableStream({ - async start(controller: ReadableStreamDefaultController) { - let accumulatedChunks = '' - try { - for await (const chunk of stream) { - 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が成功したのでチャンクをリセット + + const reader = stream.getReader() + + return new ReadableStream({ + async start(controller: ReadableStreamDefaultController) { + let accumulatedChunks = '' + try { + while (true) { + const { done, value } = await reader.read() + if (done) break + + 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) { - // console.log("accumulatedChunks: `" + accumulatedChunks + "`"); - // 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 new file mode 100644 index 0000000..6a80acb --- /dev/null +++ b/src/pages/api/local-llm.ts @@ -0,0 +1,65 @@ +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 + + 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 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 (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 + errorCode = 'LocalLLMAPIError' + } else if (err.message) { + errorMessage = err.message + } + } + + res.status(500).json({ + error: errorMessage, + errorCode: errorCode, + }) + } +} diff --git a/src/pages/api/save-chat-log.ts b/src/pages/api/save-chat-log.ts index 3dfe724..2fa3db8 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,66 @@ 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)) + // TODO: 標準化する + 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) 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"] }