added all new chanes
This commit is contained in:
parent
33d26f5af9
commit
aa9236fb92
|
|
@ -0,0 +1,112 @@
|
|||
# راهنمای تنظیم n8n Workflow برای چت
|
||||
|
||||
## گزینه 1: پاسخ ساده بدون GPT
|
||||
|
||||
### Node 1: Webhook
|
||||
- **HTTP Method**: POST
|
||||
- **Path**: `/chat` (یا هر مسیری که میخواهی)
|
||||
- **Response Mode**: Using 'Respond to Webhook' Node
|
||||
|
||||
### Node 2: Code (JavaScript)
|
||||
```javascript
|
||||
const inputData = $input.item.json;
|
||||
const userMessage = inputData.message || '';
|
||||
|
||||
// یک پاسخ ساده برمیگردانیم
|
||||
return {
|
||||
json: {
|
||||
response: `شما گفتید: "${userMessage}". این یک پاسخ تست از n8n است!`
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### Node 3: Respond to Webhook
|
||||
- **Response Body**: `{{ $json.response }}`
|
||||
|
||||
---
|
||||
|
||||
## گزینه 2: با استفاده از GPT (Message a model)
|
||||
|
||||
### Node 1: Webhook
|
||||
- **HTTP Method**: POST
|
||||
- **Path**: `/chat`
|
||||
- **Response Mode**: Using 'Respond to Webhook' Node
|
||||
|
||||
### Node 2: Code (JavaScript)
|
||||
```javascript
|
||||
const inputData = $input.item.json;
|
||||
const userMessage = inputData.message || '';
|
||||
const conversationHistory = inputData.conversationHistory || [];
|
||||
|
||||
// ساخت messages array برای GPT
|
||||
const messages = [
|
||||
{
|
||||
role: 'system',
|
||||
content: 'شما یک دستیار هوشمند فارسیزبان هستید. پاسخهای کوتاه و مفید بدهید.'
|
||||
}
|
||||
];
|
||||
|
||||
// اضافه کردن تاریخچه
|
||||
conversationHistory.forEach(msg => {
|
||||
messages.push({
|
||||
role: msg.role,
|
||||
content: msg.content
|
||||
});
|
||||
});
|
||||
|
||||
// اضافه کردن پیام فعلی
|
||||
messages.push({
|
||||
role: 'user',
|
||||
content: userMessage
|
||||
});
|
||||
|
||||
return {
|
||||
json: {
|
||||
messages: messages,
|
||||
model: 'gpt-4o-mini',
|
||||
temperature: 0.7
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### Node 3: Message a model
|
||||
- **Model**: `gpt-4o-mini`
|
||||
- **Messages**: `{{ $json.messages }}`
|
||||
- **Temperature**: `{{ $json.temperature }}`
|
||||
- **Credentials**: باید OpenAI API Key را تنظیم کنی
|
||||
|
||||
### Node 4: Code (برای فرمت کردن پاسخ)
|
||||
```javascript
|
||||
const gptResponse = $input.item.json;
|
||||
const responseText = gptResponse.choices?.[0]?.message?.content ||
|
||||
gptResponse.response ||
|
||||
'پاسخی دریافت نشد';
|
||||
|
||||
return {
|
||||
json: {
|
||||
response: responseText
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### Node 5: Respond to Webhook
|
||||
- **Response Body**: `{{ $json.response }}`
|
||||
|
||||
---
|
||||
|
||||
## نکات مهم:
|
||||
|
||||
1. **URL Webhook**: بعد از فعال کردن Webhook، URL را کپی کن و در `.env` بذار:
|
||||
```
|
||||
VITE_N8N_WEBHOOK_URL=https://your-n8n.com/webhook/chat
|
||||
```
|
||||
|
||||
2. **فعال کردن Workflow**: حتماً Workflow را فعال (Activate) کن
|
||||
|
||||
3. **تست**: میتوانی با Postman یا curl تست کنی:
|
||||
```bash
|
||||
curl -X POST https://your-n8n.com/webhook/chat \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"message":"سلام","userId":"test-user"}'
|
||||
```
|
||||
|
||||
|
|
@ -8,6 +8,7 @@
|
|||
"name": "pwa-react-app",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@n8n/chat": "^1.2.1",
|
||||
"@radix-ui/react-avatar": "^1.0.4",
|
||||
"@radix-ui/react-dialog": "^1.0.5",
|
||||
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
||||
|
|
@ -390,7 +391,6 @@
|
|||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
|
||||
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
|
|
@ -400,7 +400,6 @@
|
|||
"version": "7.28.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
|
||||
"integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
|
|
@ -449,7 +448,6 @@
|
|||
"version": "7.28.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz",
|
||||
"integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.28.5"
|
||||
|
|
@ -1653,7 +1651,6 @@
|
|||
"version": "7.28.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz",
|
||||
"integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-string-parser": "^7.27.1",
|
||||
|
|
@ -2342,6 +2339,27 @@
|
|||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@n8n/chat": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@n8n/chat/-/chat-1.2.1.tgz",
|
||||
"integrity": "sha512-zoYGr1T91fIJAmrf5d7YkhB9AIN5pXCFtB016s2PPRJvfA1QdYnJK3T8TktJ0KeGa9A+jeraxC9du3/YVfvXxg==",
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"dependencies": {
|
||||
"@n8n/design-system": "2.2.1",
|
||||
"@vueuse/core": "^10.11.0",
|
||||
"highlight.js": "11.8.0",
|
||||
"markdown-it-link-attributes": "^4.0.1",
|
||||
"uuid": "10.0.0",
|
||||
"vue": "^3.5.13",
|
||||
"vue-markdown-render": "^2.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@n8n/design-system": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@n8n/design-system/-/design-system-2.2.1.tgz",
|
||||
"integrity": "sha512-ej8xIYC4pUGgb83Bqu/uE4WYLb0NlYIztC9SRxeHcseekIddeLaEQ70uJcGOo1qMxSm6vOJh/m4m3gDgCO557w==",
|
||||
"license": "SEE LICENSE IN LICENSE.md"
|
||||
},
|
||||
"node_modules/@nodelib/fs.scandir": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||
|
|
@ -3768,6 +3786,12 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/web-bluetooth": {
|
||||
"version": "0.0.20",
|
||||
"resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz",
|
||||
"integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/ws": {
|
||||
"version": "8.18.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
|
||||
|
|
@ -4003,6 +4027,151 @@
|
|||
"vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-core": {
|
||||
"version": "3.5.26",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.26.tgz",
|
||||
"integrity": "sha512-vXyI5GMfuoBCnv5ucIT7jhHKl55Y477yxP6fc4eUswjP8FG3FFVFd41eNDArR+Uk3QKn2Z85NavjaxLxOC19/w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.28.5",
|
||||
"@vue/shared": "3.5.26",
|
||||
"entities": "^7.0.0",
|
||||
"estree-walker": "^2.0.2",
|
||||
"source-map-js": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-dom": {
|
||||
"version": "3.5.26",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.26.tgz",
|
||||
"integrity": "sha512-y1Tcd3eXs834QjswshSilCBnKGeQjQXB6PqFn/1nxcQw4pmG42G8lwz+FZPAZAby6gZeHSt/8LMPfZ4Rb+Bd/A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/compiler-core": "3.5.26",
|
||||
"@vue/shared": "3.5.26"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-sfc": {
|
||||
"version": "3.5.26",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.26.tgz",
|
||||
"integrity": "sha512-egp69qDTSEZcf4bGOSsprUr4xI73wfrY5oRs6GSgXFTiHrWj4Y3X5Ydtip9QMqiCMCPVwLglB9GBxXtTadJ3mA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.28.5",
|
||||
"@vue/compiler-core": "3.5.26",
|
||||
"@vue/compiler-dom": "3.5.26",
|
||||
"@vue/compiler-ssr": "3.5.26",
|
||||
"@vue/shared": "3.5.26",
|
||||
"estree-walker": "^2.0.2",
|
||||
"magic-string": "^0.30.21",
|
||||
"postcss": "^8.5.6",
|
||||
"source-map-js": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-sfc/node_modules/magic-string": {
|
||||
"version": "0.30.21",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
||||
"integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-ssr": {
|
||||
"version": "3.5.26",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.26.tgz",
|
||||
"integrity": "sha512-lZT9/Y0nSIRUPVvapFJEVDbEXruZh2IYHMk2zTtEgJSlP5gVOqeWXH54xDKAaFS4rTnDeDBQUYDtxKyoW9FwDw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/compiler-dom": "3.5.26",
|
||||
"@vue/shared": "3.5.26"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/reactivity": {
|
||||
"version": "3.5.26",
|
||||
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.26.tgz",
|
||||
"integrity": "sha512-9EnYB1/DIiUYYnzlnUBgwU32NNvLp/nhxLXeWRhHUEeWNTn1ECxX8aGO7RTXeX6PPcxe3LLuNBFoJbV4QZ+CFQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/shared": "3.5.26"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/runtime-core": {
|
||||
"version": "3.5.26",
|
||||
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.26.tgz",
|
||||
"integrity": "sha512-xJWM9KH1kd201w5DvMDOwDHYhrdPTrAatn56oB/LRG4plEQeZRQLw0Bpwih9KYoqmzaxF0OKSn6swzYi84e1/Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/reactivity": "3.5.26",
|
||||
"@vue/shared": "3.5.26"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/runtime-dom": {
|
||||
"version": "3.5.26",
|
||||
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.26.tgz",
|
||||
"integrity": "sha512-XLLd/+4sPC2ZkN/6+V4O4gjJu6kSDbHAChvsyWgm1oGbdSO3efvGYnm25yCjtFm/K7rrSDvSfPDgN1pHgS4VNQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/reactivity": "3.5.26",
|
||||
"@vue/runtime-core": "3.5.26",
|
||||
"@vue/shared": "3.5.26",
|
||||
"csstype": "^3.2.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/server-renderer": {
|
||||
"version": "3.5.26",
|
||||
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.26.tgz",
|
||||
"integrity": "sha512-TYKLXmrwWKSodyVuO1WAubucd+1XlLg4set0YoV+Hu8Lo79mp/YMwWV5mC5FgtsDxX3qo1ONrxFaTP1OQgy1uA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/compiler-ssr": "3.5.26",
|
||||
"@vue/shared": "3.5.26"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "3.5.26"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/shared": {
|
||||
"version": "3.5.26",
|
||||
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.26.tgz",
|
||||
"integrity": "sha512-7Z6/y3uFI5PRoKeorTOSXKcDj0MSasfNNltcslbFrPpcw6aXRUALq4IfJlaTRspiWIUOEZbrpM+iQGmCOiWe4A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@vueuse/core": {
|
||||
"version": "10.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.11.1.tgz",
|
||||
"integrity": "sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/web-bluetooth": "^0.0.20",
|
||||
"@vueuse/metadata": "10.11.1",
|
||||
"@vueuse/shared": "10.11.1",
|
||||
"vue-demi": ">=0.14.8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
}
|
||||
},
|
||||
"node_modules/@vueuse/metadata": {
|
||||
"version": "10.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.11.1.tgz",
|
||||
"integrity": "sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
}
|
||||
},
|
||||
"node_modules/@vueuse/shared": {
|
||||
"version": "10.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-10.11.1.tgz",
|
||||
"integrity": "sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"vue-demi": ">=0.14.8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
}
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.15.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||
|
|
@ -4096,7 +4265,6 @@
|
|||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
||||
"dev": true,
|
||||
"license": "Python-2.0"
|
||||
},
|
||||
"node_modules/aria-hidden": {
|
||||
|
|
@ -4649,10 +4817,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/csstype": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||
"devOptional": true,
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/data-view-buffer": {
|
||||
|
|
@ -4874,6 +5041,18 @@
|
|||
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/entities": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-7.0.0.tgz",
|
||||
"integrity": "sha512-FDWG5cmEYf2Z00IkYRhbFrwIwvdFKH07uV8dvNy0omp/Qb1xcyCWp2UDtcwJF4QZZvk0sLudP6/hAu42TaqVhQ==",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/es-abstract": {
|
||||
"version": "1.24.0",
|
||||
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz",
|
||||
|
|
@ -5264,7 +5443,6 @@
|
|||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
|
||||
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/esutils": {
|
||||
|
|
@ -5887,6 +6065,15 @@
|
|||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/highlight.js": {
|
||||
"version": "11.8.0",
|
||||
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.8.0.tgz",
|
||||
"integrity": "sha512-MedQhoqVdr0U6SSnWPzfiadUcDHfN/Wzq25AkXiQv9oiOO/sG0S7XkvpFIqWBl9Yq1UYyYOOVORs5UW2XlPyzg==",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/idb": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz",
|
||||
|
|
@ -6656,6 +6843,15 @@
|
|||
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/linkify-it": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz",
|
||||
"integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"uc.micro": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/locate-path": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
|
||||
|
|
@ -6741,6 +6937,41 @@
|
|||
"sourcemap-codec": "^1.4.8"
|
||||
}
|
||||
},
|
||||
"node_modules/markdown-it": {
|
||||
"version": "14.1.0",
|
||||
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz",
|
||||
"integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"argparse": "^2.0.1",
|
||||
"entities": "^4.4.0",
|
||||
"linkify-it": "^5.0.0",
|
||||
"mdurl": "^2.0.0",
|
||||
"punycode.js": "^2.3.1",
|
||||
"uc.micro": "^2.1.0"
|
||||
},
|
||||
"bin": {
|
||||
"markdown-it": "bin/markdown-it.mjs"
|
||||
}
|
||||
},
|
||||
"node_modules/markdown-it-link-attributes": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/markdown-it-link-attributes/-/markdown-it-link-attributes-4.0.1.tgz",
|
||||
"integrity": "sha512-pg5OK0jPLg62H4k7M9mRJLT61gUp9nvG0XveKYHMOOluASo9OEF13WlXrpAp2aj35LbedAy3QOCgQCw0tkLKAQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/markdown-it/node_modules/entities": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
||||
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
|
|
@ -6751,6 +6982,12 @@
|
|||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/mdurl": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz",
|
||||
"integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/merge2": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
||||
|
|
@ -7344,6 +7581,15 @@
|
|||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/punycode.js": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz",
|
||||
"integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/queue-microtask": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
||||
|
|
@ -8743,7 +8989,7 @@
|
|||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
|
|
@ -8753,6 +8999,12 @@
|
|||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/uc.micro": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz",
|
||||
"integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/unbox-primitive": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz",
|
||||
|
|
@ -8955,6 +9207,19 @@
|
|||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/uuid": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz",
|
||||
"integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/broofa",
|
||||
"https://github.com/sponsors/ctavan"
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "5.4.21",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
|
||||
|
|
@ -9040,6 +9305,65 @@
|
|||
"workbox-window": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vue": {
|
||||
"version": "3.5.26",
|
||||
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.26.tgz",
|
||||
"integrity": "sha512-SJ/NTccVyAoNUJmkM9KUqPcYlY+u8OVL1X5EW9RIs3ch5H2uERxyyIUI4MRxVCSOiEcupX9xNGde1tL9ZKpimA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/compiler-dom": "3.5.26",
|
||||
"@vue/compiler-sfc": "3.5.26",
|
||||
"@vue/runtime-dom": "3.5.26",
|
||||
"@vue/server-renderer": "3.5.26",
|
||||
"@vue/shared": "3.5.26"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vue-demi": {
|
||||
"version": "0.14.10",
|
||||
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
|
||||
"integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"vue-demi-fix": "bin/vue-demi-fix.js",
|
||||
"vue-demi-switch": "bin/vue-demi-switch.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@vue/composition-api": "^1.0.0-rc.1",
|
||||
"vue": "^3.0.0-0 || ^2.6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@vue/composition-api": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vue-markdown-render": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/vue-markdown-render/-/vue-markdown-render-2.3.0.tgz",
|
||||
"integrity": "sha512-ZWVVKba8t0tKBlaUGaWmNynIk38gE7Bt3psC/iN2NsqpdGY15VGfBeBvF0A8cEmwHnjNVJo2IzUUqkhhfldhtg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"markdown-it": "^14.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "^3.3.4"
|
||||
}
|
||||
},
|
||||
"node_modules/webidl-conversions": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
"format": "prettier --write \"src/**/*.{ts,tsx,json,css}\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@n8n/chat": "^1.2.1",
|
||||
"@radix-ui/react-avatar": "^1.0.4",
|
||||
"@radix-ui/react-dialog": "^1.0.5",
|
||||
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -36,32 +36,51 @@ export function useAuth() {
|
|||
}, [setSession]);
|
||||
|
||||
const signInWithGoogle = async () => {
|
||||
if (!isSupabaseConfigured) {
|
||||
throw new Error('Supabase not configured');
|
||||
}
|
||||
const { error } = await supabase.auth.signInWithOAuth({
|
||||
provider: 'google',
|
||||
options: {
|
||||
redirectTo: `${window.location.origin}/account`
|
||||
}
|
||||
});
|
||||
if (error) throw error;
|
||||
if (error) {
|
||||
console.error('Google sign in error:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const signInWithPhone = async (phone: string) => {
|
||||
if (!isSupabaseConfigured) {
|
||||
throw new Error('Supabase not configured');
|
||||
}
|
||||
const { error } = await supabase.auth.signInWithOtp({
|
||||
phone,
|
||||
options: {
|
||||
channel: 'sms'
|
||||
}
|
||||
});
|
||||
if (error) throw error;
|
||||
if (error) {
|
||||
console.error('Phone OTP error:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const verifyOTP = async (phone: string, token: string) => {
|
||||
const { error } = await supabase.auth.verifyOtp({
|
||||
if (!isSupabaseConfigured) {
|
||||
throw new Error('Supabase not configured');
|
||||
}
|
||||
const { data, error } = await supabase.auth.verifyOtp({
|
||||
phone,
|
||||
token,
|
||||
type: 'sms'
|
||||
});
|
||||
if (error) throw error;
|
||||
if (error) {
|
||||
console.error('OTP verification error:', error);
|
||||
throw error;
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
const signUp = async (email: string, password: string) => {
|
||||
|
|
@ -76,11 +95,18 @@ export function useAuth() {
|
|||
};
|
||||
|
||||
const signIn = async (email: string, password: string) => {
|
||||
const { error } = await supabase.auth.signInWithPassword({
|
||||
if (!isSupabaseConfigured) {
|
||||
throw new Error('Supabase not configured');
|
||||
}
|
||||
const { data, error } = await supabase.auth.signInWithPassword({
|
||||
email,
|
||||
password
|
||||
});
|
||||
if (error) throw error;
|
||||
if (error) {
|
||||
console.error('Sign in error:', error);
|
||||
throw error;
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
const signOut = async () => {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,40 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { supabase, isSupabaseConfigured, type Message } from '@/services/supabase';
|
||||
import {
|
||||
generateAIResponse,
|
||||
isOpenAIConfigured,
|
||||
systemMessage,
|
||||
type ChatCompletionMessage
|
||||
} from '@/services/openai';
|
||||
import { sendMessageToN8n, isN8nConfigured, type ChatMessage as N8nChatMessage } from '@/services/n8n';
|
||||
|
||||
export type ChatMessage = Message & {
|
||||
role?: 'user' | 'assistant';
|
||||
status?: 'pending' | 'sent' | 'error';
|
||||
};
|
||||
|
||||
const createLocalId = () => {
|
||||
if (typeof crypto !== 'undefined' && 'randomUUID' in crypto) {
|
||||
return crypto.randomUUID();
|
||||
}
|
||||
return `${Date.now()}-${Math.random().toString(16).slice(2, 10)}`;
|
||||
};
|
||||
|
||||
export function useChat() {
|
||||
const [messages, setMessages] = useState<Message[]>([]);
|
||||
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const conversationRef = useRef<ChatCompletionMessage[]>([systemMessage]);
|
||||
const n8nHistoryRef = useRef<N8nChatMessage[]>([]);
|
||||
const isAIEnabled = isOpenAIConfigured || isN8nConfigured;
|
||||
|
||||
useEffect(() => {
|
||||
if (isAIEnabled) {
|
||||
// بدون پیام خوشآمدگویی - کاربر باید دکمه استارت را بزند
|
||||
setMessages([]);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
loadMessages();
|
||||
|
||||
if (!isSupabaseConfigured) {
|
||||
|
|
@ -34,6 +63,10 @@ export function useChat() {
|
|||
}, []);
|
||||
|
||||
const loadMessages = async () => {
|
||||
if (isAIEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isSupabaseConfigured) {
|
||||
setMessages([]);
|
||||
setLoading(false);
|
||||
|
|
@ -51,7 +84,7 @@ export function useChat() {
|
|||
|
||||
// For now, user data will be fetched separately in the component
|
||||
// You can create a profiles table and join it here if needed
|
||||
setMessages((data || []) as Message[]);
|
||||
setMessages((data || []) as ChatMessage[]);
|
||||
} catch (error) {
|
||||
console.error('Error loading messages:', error);
|
||||
setMessages([]);
|
||||
|
|
@ -60,7 +93,77 @@ export function useChat() {
|
|||
}
|
||||
};
|
||||
|
||||
const sendMessage = async (content: string, userId: string) => {
|
||||
const sendMessage = async (content: string, userId: string, skipUserMessage = false) => {
|
||||
if (isAIEnabled) {
|
||||
const createdAt = new Date().toISOString();
|
||||
const userMessage: ChatMessage = {
|
||||
id: createLocalId(),
|
||||
user_id: userId,
|
||||
content,
|
||||
created_at: createdAt,
|
||||
role: 'user',
|
||||
status: 'pending'
|
||||
};
|
||||
|
||||
// فقط اگر skipUserMessage false باشد، پیام کاربر را نمایش بده
|
||||
if (!skipUserMessage) {
|
||||
setMessages((prev) => [userMessage, ...prev]);
|
||||
}
|
||||
|
||||
try {
|
||||
let aiContent: string;
|
||||
|
||||
// اولویت با n8n است اگر تنظیم شده باشد
|
||||
if (isN8nConfigured) {
|
||||
// اضافه کردن پیام کاربر به تاریخچه n8n
|
||||
n8nHistoryRef.current.push({ role: 'user', content, timestamp: createdAt });
|
||||
|
||||
// ارسال به n8n
|
||||
aiContent = await sendMessageToN8n(content, userId, n8nHistoryRef.current);
|
||||
|
||||
// اضافه کردن پاسخ به تاریخچه n8n
|
||||
n8nHistoryRef.current.push({
|
||||
role: 'assistant',
|
||||
content: aiContent,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
} else if (isOpenAIConfigured) {
|
||||
// استفاده از OpenAI
|
||||
conversationRef.current.push({ role: 'user', content });
|
||||
aiContent = await generateAIResponse(conversationRef.current);
|
||||
conversationRef.current.push({ role: 'assistant', content: aiContent });
|
||||
} else {
|
||||
throw new Error('هیچ سرویس هوش مصنوعی تنظیم نشده است.');
|
||||
}
|
||||
|
||||
const aiMessage: ChatMessage = {
|
||||
id: createLocalId(),
|
||||
user_id: 'ai-assistant',
|
||||
content: aiContent,
|
||||
created_at: new Date().toISOString(),
|
||||
role: 'assistant',
|
||||
status: 'sent'
|
||||
};
|
||||
|
||||
// فقط اگر پیام کاربر نمایش داده شده، status را update کن
|
||||
if (!skipUserMessage) {
|
||||
setMessages((prev) =>
|
||||
prev.map((msg) => (msg.id === userMessage.id ? { ...msg, status: 'sent' } : msg))
|
||||
);
|
||||
}
|
||||
setMessages((prev) => [aiMessage, ...prev]);
|
||||
return aiMessage;
|
||||
} catch (error) {
|
||||
// فقط اگر پیام کاربر نمایش داده شده، status را update کن
|
||||
if (!skipUserMessage) {
|
||||
setMessages((prev) =>
|
||||
prev.map((msg) => (msg.id === userMessage.id ? { ...msg, status: 'error' } : msg))
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isSupabaseConfigured) {
|
||||
throw new Error('Supabase is not configured. Cannot send messages in offline mode.');
|
||||
}
|
||||
|
|
@ -78,7 +181,7 @@ export function useChat() {
|
|||
.single();
|
||||
|
||||
if (error) throw error;
|
||||
return data;
|
||||
return data as ChatMessage;
|
||||
} catch (error) {
|
||||
console.error('Error sending message:', error);
|
||||
throw error;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,77 @@
|
|||
import { useState, useEffect, useRef } from 'react';
|
||||
|
||||
interface UseTypewriterOptions {
|
||||
text: string;
|
||||
speed?: number; // میلیثانیه بین هر کاراکتر
|
||||
onComplete?: () => void;
|
||||
}
|
||||
|
||||
export function useTypewriter({ text, speed = 30, onComplete }: UseTypewriterOptions) {
|
||||
const [displayedText, setDisplayedText] = useState('');
|
||||
const [isTyping, setIsTyping] = useState(false);
|
||||
const indexRef = useRef(0);
|
||||
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const textRef = useRef(text);
|
||||
const hasCompletedRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
// Cleanup previous timeout
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
timeoutRef.current = null;
|
||||
}
|
||||
|
||||
// If text is empty, reset everything
|
||||
if (!text || text.trim() === '') {
|
||||
setDisplayedText('');
|
||||
setIsTyping(false);
|
||||
indexRef.current = 0;
|
||||
hasCompletedRef.current = false;
|
||||
textRef.current = '';
|
||||
return;
|
||||
}
|
||||
|
||||
// Only restart if text actually changed
|
||||
const textChanged = textRef.current !== text;
|
||||
|
||||
if (textChanged) {
|
||||
// Reset when text changes
|
||||
setDisplayedText('');
|
||||
indexRef.current = 0;
|
||||
setIsTyping(true);
|
||||
hasCompletedRef.current = false;
|
||||
textRef.current = text;
|
||||
|
||||
const typeNextChar = () => {
|
||||
if (indexRef.current < text.length) {
|
||||
setDisplayedText(text.substring(0, indexRef.current + 1));
|
||||
indexRef.current += 1;
|
||||
timeoutRef.current = setTimeout(typeNextChar, speed);
|
||||
} else {
|
||||
setIsTyping(false);
|
||||
hasCompletedRef.current = true;
|
||||
if (onComplete) {
|
||||
onComplete();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Start typing immediately
|
||||
timeoutRef.current = setTimeout(typeNextChar, speed);
|
||||
} else if (hasCompletedRef.current) {
|
||||
// Text hasn't changed and already completed, show full text
|
||||
setDisplayedText(text);
|
||||
setIsTyping(false);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
timeoutRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [text, speed, onComplete]);
|
||||
|
||||
return { displayedText, isTyping };
|
||||
}
|
||||
|
||||
|
|
@ -42,15 +42,22 @@ export default function LoginPage() {
|
|||
});
|
||||
navigate('/account');
|
||||
} catch (error: any) {
|
||||
console.error('Login error:', error);
|
||||
let errorMessage = 'خطا در ورود';
|
||||
|
||||
if (error.message) {
|
||||
if (error.message.includes('Invalid login credentials')) {
|
||||
if (error) {
|
||||
const errorMsg = error.message || error.error?.message || String(error);
|
||||
|
||||
if (errorMsg.includes('Invalid login credentials') || errorMsg.includes('invalid_credentials')) {
|
||||
errorMessage = 'ایمیل یا رمز عبور اشتباه است. لطفا دوباره تلاش کنید یا ثبت نام کنید.';
|
||||
} else if (error.message.includes('Email not confirmed')) {
|
||||
} else if (errorMsg.includes('Email not confirmed') || errorMsg.includes('email_not_confirmed')) {
|
||||
errorMessage = 'لطفا ابتدا ایمیل خود را تأیید کنید.';
|
||||
} else if (errorMsg.includes('Supabase not configured')) {
|
||||
errorMessage = 'Supabase تنظیم نشده است. لطفا فایل .env را بررسی کنید.';
|
||||
} else if (errorMsg.includes('Network') || errorMsg.includes('fetch')) {
|
||||
errorMessage = 'خطا در اتصال به سرور. لطفا اتصال اینترنت خود را بررسی کنید.';
|
||||
} else {
|
||||
errorMessage = error.message;
|
||||
errorMessage = errorMsg || 'خطا در ورود. لطفا دوباره تلاش کنید.';
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -70,9 +77,13 @@ export default function LoginPage() {
|
|||
await signInWithGoogle();
|
||||
// Redirect happens automatically
|
||||
} catch (error: any) {
|
||||
console.error('Google login error:', error);
|
||||
const errorMsg = error?.message || error?.error?.message || String(error) || 'خطا در ورود با گوگل';
|
||||
toast({
|
||||
title: 'خطا',
|
||||
description: error.message || 'خطا در ورود با گوگل',
|
||||
description: errorMsg.includes('Supabase not configured')
|
||||
? 'Supabase تنظیم نشده است. لطفا فایل .env را بررسی کنید.'
|
||||
: errorMsg,
|
||||
variant: 'destructive'
|
||||
});
|
||||
setIsLoading(false);
|
||||
|
|
@ -99,9 +110,23 @@ export default function LoginPage() {
|
|||
description: 'کد تأیید ارسال شد'
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('Phone OTP error:', error);
|
||||
const errorMsg = error?.message || error?.error?.message || String(error) || 'خطا در ارسال کد';
|
||||
let errorMessage = 'خطا در ارسال کد';
|
||||
|
||||
if (errorMsg.includes('Supabase not configured')) {
|
||||
errorMessage = 'Supabase تنظیم نشده است. لطفا فایل .env را بررسی کنید.';
|
||||
} else if (errorMsg.includes('Invalid phone number') || errorMsg.includes('invalid_phone')) {
|
||||
errorMessage = 'شماره تلفن نامعتبر است. لطفا شماره را با فرمت صحیح وارد کنید.';
|
||||
} else if (errorMsg.includes('Network') || errorMsg.includes('fetch')) {
|
||||
errorMessage = 'خطا در اتصال به سرور. لطفا اتصال اینترنت خود را بررسی کنید.';
|
||||
} else {
|
||||
errorMessage = errorMsg;
|
||||
}
|
||||
|
||||
toast({
|
||||
title: 'خطا',
|
||||
description: error.message || 'خطا در ارسال کد',
|
||||
description: errorMessage,
|
||||
variant: 'destructive'
|
||||
});
|
||||
} finally {
|
||||
|
|
@ -129,9 +154,25 @@ export default function LoginPage() {
|
|||
});
|
||||
navigate('/account');
|
||||
} catch (error: any) {
|
||||
console.error('OTP verification error:', error);
|
||||
const errorMsg = error?.message || error?.error?.message || String(error) || 'کد تأیید نامعتبر است';
|
||||
let errorMessage = 'کد تأیید نامعتبر است';
|
||||
|
||||
if (errorMsg.includes('Supabase not configured')) {
|
||||
errorMessage = 'Supabase تنظیم نشده است. لطفا فایل .env را بررسی کنید.';
|
||||
} else if (errorMsg.includes('Invalid OTP') || errorMsg.includes('invalid_token')) {
|
||||
errorMessage = 'کد تأیید نامعتبر است. لطفا دوباره تلاش کنید.';
|
||||
} else if (errorMsg.includes('expired')) {
|
||||
errorMessage = 'کد تأیید منقضی شده است. لطفا کد جدید درخواست کنید.';
|
||||
} else if (errorMsg.includes('Network') || errorMsg.includes('fetch')) {
|
||||
errorMessage = 'خطا در اتصال به سرور. لطفا اتصال اینترنت خود را بررسی کنید.';
|
||||
} else {
|
||||
errorMessage = errorMsg;
|
||||
}
|
||||
|
||||
toast({
|
||||
title: 'خطا',
|
||||
description: error.message || 'کد تأیید نامعتبر است',
|
||||
description: errorMessage,
|
||||
variant: 'destructive'
|
||||
});
|
||||
} finally {
|
||||
|
|
|
|||
|
|
@ -5,8 +5,9 @@ import { Input } from '@/components/ui/input';
|
|||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
||||
import { useAuth } from '@/hooks/useAuth';
|
||||
import { useChat } from '@/hooks/useChat';
|
||||
import { useTypewriter } from '@/hooks/useTypewriter';
|
||||
import { useToast } from '@/components/ui/use-toast';
|
||||
import { Send, MessageSquare, Loader2 } from 'lucide-react';
|
||||
import { Send, MessageSquare, Loader2, Play } from 'lucide-react';
|
||||
|
||||
export default function ChatPage() {
|
||||
const { user } = useAuth();
|
||||
|
|
@ -15,13 +16,14 @@ export default function ChatPage() {
|
|||
const [message, setMessage] = useState('');
|
||||
const [isSending, setIsSending] = useState(false);
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
const typedMessagesRef = useRef<Set<string>>(new Set()); // Track which messages have been typed
|
||||
|
||||
useEffect(() => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||
}, [messages]);
|
||||
|
||||
const handleSend = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
const handleSend = async (e?: React.FormEvent, initialMessage?: string, skipUserMessage = false) => {
|
||||
e?.preventDefault();
|
||||
if (!user) {
|
||||
toast({
|
||||
title: 'خطا',
|
||||
|
|
@ -31,18 +33,26 @@ export default function ChatPage() {
|
|||
return;
|
||||
}
|
||||
|
||||
if (!message.trim()) {
|
||||
const messageToSend = initialMessage || message;
|
||||
if (!messageToSend.trim()) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSending(true);
|
||||
try {
|
||||
await sendMessage(message, user.id);
|
||||
setMessage('');
|
||||
// @ts-ignore - skipUserMessage parameter exists
|
||||
await sendMessage(messageToSend, user.id, skipUserMessage);
|
||||
if (!initialMessage) {
|
||||
setMessage('');
|
||||
}
|
||||
} catch (error) {
|
||||
const description =
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: 'خطا در ارسال پیام';
|
||||
toast({
|
||||
title: 'خطا',
|
||||
description: 'خطا در ارسال پیام',
|
||||
description,
|
||||
variant: 'destructive'
|
||||
});
|
||||
} finally {
|
||||
|
|
@ -50,7 +60,13 @@ export default function ChatPage() {
|
|||
}
|
||||
};
|
||||
|
||||
const handleStartChat = () => {
|
||||
// skipUserMessage = true یعنی پیام کاربر نمایش داده نمیشود
|
||||
handleSend(undefined, 'سلام', true);
|
||||
};
|
||||
|
||||
const getUserDisplayName = (msg: typeof messages[0]) => {
|
||||
if (msg?.role === 'assistant') return 'دستیار هوشمند';
|
||||
if (msg.user_id === user?.id) return 'شما';
|
||||
// For other users, show a simplified identifier
|
||||
// In production, you'd fetch from a profiles table
|
||||
|
|
@ -61,12 +77,113 @@ export default function ChatPage() {
|
|||
};
|
||||
|
||||
const getUserInitials = (msg: typeof messages[0]) => {
|
||||
if (msg?.role === 'assistant') return 'هو';
|
||||
const name = getUserDisplayName(msg);
|
||||
if (name === 'شما') return 'ش';
|
||||
if (name.length >= 2) return name.substring(0, 2);
|
||||
return 'ک';
|
||||
};
|
||||
|
||||
// Component for message bubble with typewriter effect for AI messages
|
||||
const MessageBubble = ({
|
||||
message,
|
||||
isOwnMessage,
|
||||
isAssistant,
|
||||
getUserDisplayName,
|
||||
getUserInitials
|
||||
}: {
|
||||
message: typeof messages[0];
|
||||
isOwnMessage: boolean;
|
||||
isAssistant: boolean;
|
||||
getUserDisplayName: (msg: typeof messages[0]) => string;
|
||||
getUserInitials: (msg: typeof messages[0]) => string;
|
||||
}) => {
|
||||
// Check if this message has already been typed
|
||||
const hasBeenTyped = typedMessagesRef.current.has(message.id);
|
||||
|
||||
// If it's an assistant message and hasn't been typed yet, use typewriter
|
||||
const shouldAnimate = isAssistant && !hasBeenTyped && !!message.content;
|
||||
|
||||
// Only use typewriter for assistant messages that haven't been typed yet
|
||||
const typewriterResult = useTypewriter({
|
||||
text: shouldAnimate ? message.content : '',
|
||||
speed: 10, // سرعت تایپ سریعتر (میلیثانیه)
|
||||
onComplete: () => {
|
||||
// Mark this message as typed when animation completes
|
||||
if (isAssistant && message.id) {
|
||||
typedMessagesRef.current.add(message.id);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Determine final text to display
|
||||
// Priority: animated text (if available) > message.content
|
||||
let finalText: string;
|
||||
if (isAssistant && shouldAnimate) {
|
||||
// For assistant messages with animation, use displayedText if it exists, otherwise fallback to content
|
||||
finalText = typewriterResult.displayedText.length > 0
|
||||
? typewriterResult.displayedText
|
||||
: message.content || '';
|
||||
} else {
|
||||
// For user messages or already-typed assistant messages, show full content
|
||||
finalText = message.content || '';
|
||||
}
|
||||
|
||||
const showCursor = isAssistant && shouldAnimate && typewriterResult.isTyping;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex gap-3 ${isOwnMessage ? 'flex-row-reverse' : 'flex-row'}`}
|
||||
>
|
||||
<Avatar className="h-8 w-8 flex-shrink-0">
|
||||
<AvatarImage src={message.user?.avatar_url} />
|
||||
<AvatarFallback className="text-xs">
|
||||
{getUserInitials(message)}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className={`flex-1 ${isOwnMessage ? 'text-left' : 'text-right'}`}>
|
||||
<div className="flex items-center gap-2 mb-1 flex-wrap">
|
||||
<span className="text-xs font-medium">
|
||||
{getUserDisplayName(message)}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{new Date(message.created_at).toLocaleTimeString('fa-IR', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className={`rounded-lg p-3 ${
|
||||
isOwnMessage
|
||||
? 'bg-primary text-primary-foreground ml-auto'
|
||||
: isAssistant
|
||||
? 'bg-secondary text-secondary-foreground'
|
||||
: 'bg-muted'
|
||||
}`}
|
||||
style={{
|
||||
maxWidth: '85%',
|
||||
marginRight: isOwnMessage ? '0' : 'auto',
|
||||
marginLeft: isOwnMessage ? 'auto' : '0'
|
||||
}}
|
||||
>
|
||||
<p className="text-xs whitespace-pre-wrap break-words">
|
||||
{finalText}
|
||||
{showCursor && (
|
||||
<span className="inline-block w-2 h-4 bg-current ml-1 animate-pulse">|</span>
|
||||
)}
|
||||
</p>
|
||||
{message.status === 'error' && (
|
||||
<span className="mt-2 block text-[10px] text-destructive">
|
||||
ارسال ناموفق بود. دوباره تلاش کنید.
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-background to-muted p-4 pb-20">
|
||||
<div className="mx-auto max-w-sm">
|
||||
|
|
@ -94,49 +211,46 @@ export default function ChatPage() {
|
|||
<>
|
||||
<div className="flex-1 overflow-y-auto space-y-4 mb-4 pr-2">
|
||||
{messages.length === 0 ? (
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
هیچ پیامی وجود ندارد. اولین پیام را ارسال کنید!
|
||||
<div className="flex flex-col items-center justify-center h-full py-8">
|
||||
<div className="text-center mb-6">
|
||||
<MessageSquare className="h-16 w-16 text-muted-foreground mx-auto mb-4 opacity-50" />
|
||||
<p className="text-muted-foreground mb-2">به چت هوشمند خوش آمدید</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
برای شروع گفتگو، دکمه زیر را بزنید
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
onClick={handleStartChat}
|
||||
disabled={isSending || !user}
|
||||
size="lg"
|
||||
className="gap-2"
|
||||
>
|
||||
{isSending ? (
|
||||
<>
|
||||
<Loader2 className="h-5 w-5 animate-spin" />
|
||||
در حال اتصال...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Play className="h-5 w-5" />
|
||||
شروع گفتگو
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
messages.map((msg) => {
|
||||
const isOwnMessage = msg.user_id === user?.id;
|
||||
const isOwnMessage = msg.user_id === user?.id && msg.role !== 'assistant';
|
||||
const isAssistant = msg.role === 'assistant';
|
||||
return (
|
||||
<div
|
||||
<MessageBubble
|
||||
key={msg.id}
|
||||
className={`flex gap-3 ${isOwnMessage ? 'flex-row-reverse' : 'flex-row'}`}
|
||||
>
|
||||
<Avatar className="h-8 w-8 flex-shrink-0">
|
||||
<AvatarImage src={msg.user?.avatar_url} />
|
||||
<AvatarFallback className="text-xs">
|
||||
{getUserInitials(msg)}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div
|
||||
className={`flex-1 ${isOwnMessage ? 'text-left' : 'text-right'}`}
|
||||
>
|
||||
<div className="flex items-center gap-2 mb-1 flex-wrap">
|
||||
<span className="text-xs font-medium">
|
||||
{getUserDisplayName(msg)}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{new Date(msg.created_at).toLocaleTimeString('fa-IR', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className={`rounded-lg p-3 ${
|
||||
isOwnMessage
|
||||
? 'bg-primary text-primary-foreground ml-auto'
|
||||
: 'bg-muted'
|
||||
}`}
|
||||
style={{ maxWidth: '85%', marginRight: isOwnMessage ? '0' : 'auto', marginLeft: isOwnMessage ? 'auto' : '0' }}
|
||||
>
|
||||
<p className="text-xs whitespace-pre-wrap break-words">{msg.content}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
message={msg}
|
||||
isOwnMessage={isOwnMessage}
|
||||
isAssistant={isAssistant}
|
||||
getUserDisplayName={getUserDisplayName}
|
||||
getUserInitials={getUserInitials}
|
||||
/>
|
||||
);
|
||||
})
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,297 @@
|
|||
// URL پیشفرض n8n chat flow
|
||||
const DEFAULT_N8N_CHAT_URL = 'https://nn.feep.ir/webhook/ad03737e-f857-4b5c-87d0-601327b2081c/chat';
|
||||
const n8nWebhookUrl = import.meta.env.VITE_N8N_WEBHOOK_URL || DEFAULT_N8N_CHAT_URL;
|
||||
const n8nApiKey = import.meta.env.VITE_N8N_API_KEY || '';
|
||||
|
||||
export type ChatMessage = {
|
||||
role: 'user' | 'assistant';
|
||||
content: string;
|
||||
timestamp?: string;
|
||||
};
|
||||
|
||||
export const isN8nConfigured = Boolean(n8nWebhookUrl);
|
||||
|
||||
// ذخیره sessionId برای هر کاربر (برای حفظ context در Simple Memory)
|
||||
const sessionIdMap = new Map<string, string>();
|
||||
|
||||
// تولید یا دریافت sessionId برای یک کاربر
|
||||
const getOrCreateSessionId = (userId: string): string => {
|
||||
if (!sessionIdMap.has(userId)) {
|
||||
// تولید sessionId جدید (UUID-like)
|
||||
const sessionId = `${userId}-${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;
|
||||
sessionIdMap.set(userId, sessionId);
|
||||
console.log('🔑 ایجاد sessionId جدید:', sessionId, 'برای کاربر:', userId);
|
||||
}
|
||||
return sessionIdMap.get(userId)!;
|
||||
};
|
||||
|
||||
// در development از proxy استفاده میکنیم، در production از URL مستقیم
|
||||
const getN8nUrl = () => {
|
||||
// اگر در development هستیم و URL شامل nn.feep.ir است، از proxy استفاده کن
|
||||
const useProxy = import.meta.env.DEV && n8nWebhookUrl.includes('nn.feep.ir');
|
||||
|
||||
if (useProxy) {
|
||||
console.log('🔀 استفاده از proxy برای دور زدن CORS');
|
||||
// استخراج مسیر webhook از URL کامل
|
||||
try {
|
||||
const url = new URL(n8nWebhookUrl);
|
||||
const urlPath = url.pathname;
|
||||
const proxyUrl = `/api/n8n${urlPath}`;
|
||||
console.log('📍 مسیر proxy:', proxyUrl, 'از URL اصلی:', n8nWebhookUrl);
|
||||
return proxyUrl;
|
||||
} catch (error) {
|
||||
console.error('❌ خطا در parse کردن URL:', error);
|
||||
// اگر URL معتبر نیست، از URL مستقیم استفاده کن
|
||||
return n8nWebhookUrl;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('🌐 استفاده از URL مستقیم:', n8nWebhookUrl);
|
||||
return n8nWebhookUrl;
|
||||
};
|
||||
|
||||
/**
|
||||
* ارسال پیام به n8n و دریافت پاسخ
|
||||
* @param message - پیام کاربر
|
||||
* @param userId - شناسه کاربر
|
||||
* @param conversationHistory - تاریخچه مکالمه (اختیاری)
|
||||
* @returns پاسخ از n8n
|
||||
*/
|
||||
export async function sendMessageToN8n(
|
||||
message: string,
|
||||
userId: string,
|
||||
_conversationHistory?: ChatMessage[] // n8n Chat با Simple Memory خودش session را مدیریت میکند، این پارامتر برای backward compatibility است
|
||||
): Promise<string> {
|
||||
if (!isN8nConfigured) {
|
||||
throw new Error(
|
||||
'n8n تنظیم نشده است. مقدار VITE_N8N_WEBHOOK_URL را در فایل .env تنظیم کنید.'
|
||||
);
|
||||
}
|
||||
|
||||
// فرمت n8n Chat Trigger - این workflow از "When chat message received" استفاده میکند
|
||||
// فرمت مورد نیاز: { sessionId, action: "sendMessage", chatInput }
|
||||
const sessionId = getOrCreateSessionId(userId);
|
||||
|
||||
const payload = {
|
||||
sessionId: sessionId,
|
||||
action: 'sendMessage',
|
||||
chatInput: message
|
||||
};
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json'
|
||||
};
|
||||
|
||||
// اگر API Key تنظیم شده باشد، اضافه میکنیم
|
||||
if (n8nApiKey) {
|
||||
headers['Authorization'] = `Bearer ${n8nApiKey}`;
|
||||
// یا میتوانید از header دیگری استفاده کنید
|
||||
// headers['X-API-Key'] = n8nApiKey;
|
||||
}
|
||||
|
||||
try {
|
||||
const actualUrl = getN8nUrl();
|
||||
console.log('📤 ارسال به n8n:', { url: actualUrl, originalUrl: n8nWebhookUrl, payload });
|
||||
|
||||
const response = await fetch(actualUrl, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(payload),
|
||||
// اضافه کردن mode برای جلوگیری از CORS issues
|
||||
mode: 'cors',
|
||||
credentials: 'omit'
|
||||
});
|
||||
|
||||
console.log('📥 پاسخ از n8n:', { status: response.status, ok: response.ok });
|
||||
|
||||
if (!response.ok) {
|
||||
let errorText = '';
|
||||
try {
|
||||
errorText = await response.text();
|
||||
// اگر پاسخ HTML است (مثل خطای 500)، پیام بهتری نمایش بده
|
||||
if (errorText.trim().startsWith('<!DOCTYPE') || errorText.includes('<html')) {
|
||||
console.error('❌ خطا از n8n (HTML Error Page):', { status: response.status });
|
||||
throw new Error(
|
||||
`خطا در n8n workflow (${response.status}): خطای داخلی سرور. لطفا workflow را در n8n بررسی کنید:\n` +
|
||||
`1. آیا workflow فعال است؟\n` +
|
||||
`2. آیا Message a model node درست تنظیم شده؟\n` +
|
||||
`3. آیا API Key OpenAI درست است؟\n` +
|
||||
`4. آیا Respond to Webhook node وجود دارد؟`
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof Error && e.message.includes('خطا در n8n workflow')) {
|
||||
throw e;
|
||||
}
|
||||
errorText = response.statusText;
|
||||
}
|
||||
console.error('❌ خطا از n8n:', { status: response.status, errorText });
|
||||
throw new Error(
|
||||
`خطا در ارتباط با n8n (${response.status}): ${errorText.substring(0, 200) || response.statusText}`
|
||||
);
|
||||
}
|
||||
|
||||
let data;
|
||||
try {
|
||||
// بررسی Content-Type
|
||||
const contentType = response.headers.get('content-type') || '';
|
||||
console.log('📋 Content-Type:', contentType);
|
||||
|
||||
const responseText = await response.text();
|
||||
console.log('📄 پاسخ خام از n8n (اولین 500 کاراکتر):', responseText.substring(0, 500));
|
||||
|
||||
if (!responseText || responseText.trim() === '') {
|
||||
throw new Error('پاسخ خالی از n8n دریافت شد.');
|
||||
}
|
||||
|
||||
// اگر پاسخ HTML است (خطای 500)، خطا بده
|
||||
if (responseText.trim().startsWith('<!DOCTYPE') || responseText.includes('<html')) {
|
||||
console.error('❌ پاسخ HTML دریافت شد (خطای سرور)');
|
||||
throw new Error(
|
||||
'n8n خطای سرور برمیگرداند. لطفا بررسی کنید:\n' +
|
||||
'1. آیا workflow فعال است؟\n' +
|
||||
'2. آیا همه node ها درست تنظیم شدهاند؟\n' +
|
||||
'3. آیا Respond to Webhook node وجود دارد و درست تنظیم شده؟'
|
||||
);
|
||||
}
|
||||
|
||||
// تلاش برای parse کردن JSON
|
||||
try {
|
||||
data = JSON.parse(responseText);
|
||||
} catch (jsonError) {
|
||||
// اگر JSON نیست، ممکنه string ساده باشه
|
||||
console.warn('⚠️ پاسخ JSON نیست، به عنوان string استفاده میشود');
|
||||
return responseText.trim();
|
||||
}
|
||||
} catch (parseError) {
|
||||
if (parseError instanceof Error && parseError.message.includes('n8n خطای سرور')) {
|
||||
throw parseError;
|
||||
}
|
||||
console.error('❌ خطا در parse کردن پاسخ:', parseError);
|
||||
throw new Error('فرمت پاسخ n8n نامعتبر است (JSON parse failed): ' + String(parseError));
|
||||
}
|
||||
|
||||
console.log('📦 داده parse شده از n8n:', data);
|
||||
|
||||
// n8n Chat Trigger + AI Agent معمولاً پاسخ را در فرمتهای مختلف برمیگرداند
|
||||
// این فرمتهای رایج را پشتیبانی میکنیم:
|
||||
|
||||
// 0. فرمت AI Agent output (اولویت اول برای n8n chat)
|
||||
if (data.output && typeof data.output === 'string' && data.output.trim()) {
|
||||
console.log('✅ پاسخ از AI Agent (output):', data.output);
|
||||
return data.output.trim();
|
||||
}
|
||||
|
||||
// 0.1. فرمت AI Agent با object output
|
||||
if (data.output && typeof data.output === 'object') {
|
||||
// بررسی فیلدهای مختلف در output
|
||||
if (data.output.output && typeof data.output.output === 'string' && data.output.output.trim()) {
|
||||
console.log('✅ پاسخ از AI Agent (output.output):', data.output.output);
|
||||
return data.output.output.trim();
|
||||
}
|
||||
if (data.output.text && typeof data.output.text === 'string' && data.output.text.trim()) {
|
||||
console.log('✅ پاسخ از AI Agent (output.text):', data.output.text);
|
||||
return data.output.text.trim();
|
||||
}
|
||||
if (data.output.response && typeof data.output.response === 'string' && data.output.response.trim()) {
|
||||
console.log('✅ پاسخ از AI Agent (output.response):', data.output.response);
|
||||
return data.output.response.trim();
|
||||
}
|
||||
}
|
||||
|
||||
// 1. اگر پاسخ از Message a model (GPT) با Simplify Output خاموش است
|
||||
if (data.choices && Array.isArray(data.choices) && data.choices.length > 0) {
|
||||
const content = data.choices[0]?.message?.content;
|
||||
if (content && content.trim()) {
|
||||
console.log('✅ پاسخ از GPT (choices):', content);
|
||||
return content.trim();
|
||||
}
|
||||
}
|
||||
|
||||
// 2. اگر Simplify Output فعال است - پاسخ مستقیم از GPT
|
||||
if (data.response && typeof data.response === 'string' && data.response.trim()) {
|
||||
console.log('✅ پاسخ از n8n (response):', data.response);
|
||||
return data.response.trim();
|
||||
}
|
||||
|
||||
// 3. اگر response خالی است، ببین آیا در جای دیگری هست
|
||||
if (data.response === '' || data.response === null) {
|
||||
// شاید پاسخ در جای دیگری باشد
|
||||
if (data.choices?.[0]?.message?.content) {
|
||||
const content = data.choices[0].message.content;
|
||||
if (content && content.trim()) {
|
||||
console.log('✅ پاسخ از GPT (در choices):', content);
|
||||
return content.trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. فرمتهای دیگر
|
||||
if (typeof data === 'string' && data.trim()) {
|
||||
return data.trim();
|
||||
}
|
||||
|
||||
if (data.message && typeof data.message === 'string' && data.message.trim()) {
|
||||
return data.message.trim();
|
||||
}
|
||||
|
||||
if (data.text && typeof data.text === 'string' && data.text.trim()) {
|
||||
return data.text.trim();
|
||||
}
|
||||
|
||||
if (data.data?.response && typeof data.data.response === 'string' && data.data.response.trim()) {
|
||||
return data.data.response.trim();
|
||||
}
|
||||
|
||||
if (data.data?.message && typeof data.data.message === 'string' && data.data.message.trim()) {
|
||||
return data.data.message.trim();
|
||||
}
|
||||
|
||||
// 5. اگر پاسخ یک object است و هیچ فیلد متنی نداره، خطا بده
|
||||
if (typeof data === 'object') {
|
||||
// بررسی عمیقتر برای پیدا کردن متن
|
||||
const findTextInObject = (obj: any): string | null => {
|
||||
if (typeof obj === 'string' && obj.trim()) {
|
||||
return obj.trim();
|
||||
}
|
||||
if (Array.isArray(obj)) {
|
||||
for (const item of obj) {
|
||||
const text = findTextInObject(item);
|
||||
if (text) return text;
|
||||
}
|
||||
}
|
||||
if (obj && typeof obj === 'object') {
|
||||
for (const key in obj) {
|
||||
if (key === 'content' || key === 'text' || key === 'message' || key === 'response') {
|
||||
const text = findTextInObject(obj[key]);
|
||||
if (text) return text;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const foundText = findTextInObject(data);
|
||||
if (foundText) {
|
||||
console.log('✅ متن پیدا شد در object:', foundText);
|
||||
return foundText;
|
||||
}
|
||||
|
||||
// اگر هیچ متنی پیدا نشد، خطا بده
|
||||
console.error('❌ هیچ متن معتبری در پاسخ پیدا نشد:', JSON.stringify(data, null, 2));
|
||||
throw new Error('پاسخ از n8n خالی است یا فرمت نامعتبر دارد. لطفا Respond to Webhook را بررسی کنید.');
|
||||
}
|
||||
|
||||
throw new Error('فرمت پاسخ n8n نامعتبر است. ساختار: ' + JSON.stringify(data));
|
||||
} catch (error) {
|
||||
console.error('❌ خطا در sendMessageToN8n:', error);
|
||||
if (error instanceof TypeError && error.message.includes('fetch')) {
|
||||
throw new Error('خطا در اتصال به n8n. لطفا بررسی کنید:\n1. URL صحیح است؟\n2. n8n در دسترس است؟\n3. مشکل CORS وجود دارد؟');
|
||||
}
|
||||
if (error instanceof Error) {
|
||||
throw error;
|
||||
}
|
||||
throw new Error('خطا در ارتباط با n8n: ' + String(error));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
const openAiApiKey = import.meta.env.VITE_OPENAI_API_KEY || '';
|
||||
const openAiBaseUrl = import.meta.env.VITE_OPENAI_BASE_URL || 'https://api.openai.com/v1';
|
||||
const openAiModel = import.meta.env.VITE_OPENAI_MODEL || 'gpt-4o-mini';
|
||||
const openAiOrg = import.meta.env.VITE_OPENAI_ORG_ID || '';
|
||||
const openAiProject = import.meta.env.VITE_OPENAI_PROJECT_ID || '';
|
||||
|
||||
export type ChatCompletionRole = 'system' | 'user' | 'assistant';
|
||||
|
||||
export type ChatCompletionMessage = {
|
||||
role: ChatCompletionRole;
|
||||
content: string;
|
||||
};
|
||||
|
||||
export const isOpenAIConfigured = Boolean(openAiApiKey);
|
||||
|
||||
const defaultSystemPrompt =
|
||||
import.meta.env.VITE_OPENAI_SYSTEM_PROMPT ||
|
||||
'You are an AI assistant that responds in Persian (Farsi) with concise, helpful answers for users of a progressive web app.';
|
||||
|
||||
export const systemMessage: ChatCompletionMessage = {
|
||||
role: 'system',
|
||||
content: defaultSystemPrompt,
|
||||
};
|
||||
|
||||
export async function generateAIResponse(messages: ChatCompletionMessage[]): Promise<string> {
|
||||
if (!isOpenAIConfigured) {
|
||||
throw new Error('کلید OpenAI تعریف نشده است. مقدار VITE_OPENAI_API_KEY را تنظیم کنید.');
|
||||
}
|
||||
|
||||
const payload = {
|
||||
model: openAiModel,
|
||||
messages: messages.length ? messages : [systemMessage],
|
||||
temperature: 0.3,
|
||||
max_tokens: 800,
|
||||
};
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${openAiApiKey}`,
|
||||
};
|
||||
|
||||
if (openAiOrg) {
|
||||
headers['OpenAI-Organization'] = openAiOrg;
|
||||
}
|
||||
|
||||
if (openAiProject) {
|
||||
headers['OpenAI-Project'] = openAiProject;
|
||||
}
|
||||
|
||||
const response = await fetch(`${openAiBaseUrl}/chat/completions`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorBody = await response.text();
|
||||
throw new Error(`خطا در دریافت پاسخ از GPT: ${errorBody}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const text: string | undefined = data?.choices?.[0]?.message?.content?.trim();
|
||||
|
||||
if (!text) {
|
||||
throw new Error('پاسخی از هوش مصنوعی دریافت نشد.');
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
|
|
@ -49,12 +49,29 @@
|
|||
}
|
||||
}
|
||||
|
||||
/* Custom Font - فونت سفارشی */
|
||||
/* Custom Font - فونت سفارشی شبنم */
|
||||
@font-face {
|
||||
font-family: 'IRAN Rounded';
|
||||
src: url('/fonts/IRAN Rounded.ttf') format('truetype');
|
||||
font-weight: normal;
|
||||
font-family: 'Shabnam';
|
||||
src: url('/fonts/shabnam/Shabnam-Light.ttf') format('truetype');
|
||||
font-weight: 300;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Shabnam';
|
||||
src: url('/fonts/shabnam/Shabnam.ttf') format('truetype');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Shabnam';
|
||||
src: url('/fonts/shabnam/Shabnam-Bold.ttf') format('truetype');
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@layer base {
|
||||
|
|
@ -64,7 +81,7 @@
|
|||
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
font-family: 'IRAN Rounded', 'Tahoma', 'Arial', system-ui, -apple-system, sans-serif;
|
||||
font-family: 'Shabnam', 'Tahoma', 'Arial', system-ui, -apple-system, sans-serif;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import defaultTheme from 'tailwindcss/defaultTheme';
|
||||
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
darkMode: ['class'],
|
||||
|
|
@ -7,6 +9,9 @@ export default {
|
|||
],
|
||||
theme: {
|
||||
extend: {
|
||||
fontFamily: {
|
||||
sans: ['Shabnam', 'Tahoma', 'Arial', ...defaultTheme.fontFamily.sans],
|
||||
},
|
||||
colors: {
|
||||
border: 'hsl(var(--border))',
|
||||
input: 'hsl(var(--input))',
|
||||
|
|
|
|||
|
|
@ -48,6 +48,29 @@ export default defineConfig({
|
|||
alias: {
|
||||
'@': path.resolve(__dirname, './src')
|
||||
}
|
||||
},
|
||||
server: {
|
||||
proxy: {
|
||||
'/api/n8n': {
|
||||
target: 'https://nn.feep.ir',
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => {
|
||||
// تبدیل /api/n8n/webhook/... به /webhook/...
|
||||
const rewritten = path.replace(/^\/api\/n8n/, '');
|
||||
console.log(`[Proxy] Rewriting: ${path} -> ${rewritten}`);
|
||||
return rewritten;
|
||||
},
|
||||
secure: true,
|
||||
configure: (proxy, _options) => {
|
||||
proxy.on('error', (err, _req, res) => {
|
||||
console.log('[Proxy] Error:', err);
|
||||
});
|
||||
proxy.on('proxyReq', (proxyReq, req, _res) => {
|
||||
console.log('[Proxy] Request:', req.method, req.url, '->', proxyReq.path);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue