1 line
178 KiB
JSON
1 line
178 KiB
JSON
{"repo": "0xxue/nexus-ai", "files": {".github/workflows/ci.yml": {"2916bcc3690cc891de80d898da649cc67ba3cfb9": {"t": 1785745257.854329, "n": 5238, "h": "b79dd62729c2dbb6", "c": "name: CI\n\non:\n push:\n branches: [master, main]\n pull_request:\n branches: [master, main]\n\njobs:\n # ══════════════════════════════════════\n # Backend — Python lint + test\n # ══════════════════════════════════════\n backend:\n runs-on: ubuntu-latest\n defaults:\n run:\n working-directory: backend\n\n services:\n postgres:\n image: pgvector/pgvector:pg16\n env:\n POSTGRES_USER: postgres\n POSTGRES_PASSWORD: postgres\n POSTGRES_DB: ai_qa_test\n ports:\n - 5432:5432\n options: >-\n --health-cmd pg_isready\n --health-interval 10s\n --health-timeout 5s\n --health-retries 5\n\n redis:\n image: redis:7-alpine\n ports:\n - 6379:6379\n options: >-\n --health-cmd \"redis-cli ping\"\n --health-interval 10s\n --health-timeout 5s\n --health-retries 5\n\n steps:\n - uses: actions/checkout@v4\n\n - uses: actions/setup-python@v5\n with:\n python-version: \"3.11\"\n cache: pip\n\n - name: Install dependencies\n run: |\n pip install -r requirements.txt\n pip install ruff pytest pytest-asyncio httpx\n\n - name: Lint (ruff)\n run: ruff check app/ --select F --ignore F401,F841 || true\n\n - name: Unit tests\n run: python -m pytest tests/unit/ -v --tb=short -p no:pytest_ethereum -p no:web3\n\n - name: Type check (imports)\n run: |\n python -c \"\n from app.core.langgraph.state import QAState;\n from app.models import Base, User, Conversation, Message, BotMessage, BotScene, BotPreference;\n from app.services.bot.brain import think, BOT_PERSONAS;\n from app.services.bot.tools import get_tool_definitions;\n from app.services.bot.persistence import BotPersistenceService;\n from app.services.conversation import ConversationService;\n from app.services.llm import call_llm;\n from app.api.v1.router import api_router;\n print('All imports OK')\n \"\n\n - name: Run migrations\n env:\n DATABASE_URL: postgresql+asyncpg://postgres:postgres@localhost:5432/ai_qa_test\n REDIS_URL: redis://localhost:6379/0\n JWT_SECRET: test-secret\n DEEPSEEK_API_KEY: test\n run: alembic upgrade head\n\n - name: Test database schema\n env:\n DATABASE_URL: postgresql+asyncpg://postgres:postgres@localhost:5432/ai_qa_test\n REDIS_URL: redis://localhost:6379/0\n JWT_SECRET: test-secret\n DEEPSEEK_API_KEY: test\n run: |\n python << 'PYEOF'\n import asyncio\n from app.services.database import init_db, _session_factory\n async def check():\n await init_db()\n from app.services.database import _session_factory as sf\n async with sf() as s:\n from sqlalchemy import text\n r = await s.execute(text(\"SELECT tablename FROM pg_tables WHERE schemaname='public' ORDER BY tablename\"))\n tables = [row[0] for row in r]\n assert len(tables) >= 13, f\"Expected 13+ tables, got {len(tables)}: {tables}\"\n print(f\"OK: {len(tables)} tables\")\n asyncio.run(check())\n PYEOF\n\n # ══════════════════════════════════════\n # Frontend — lint + build\n # ══════════════════════════════════════\n frontend:\n runs-on: ubuntu-latest\n\n steps:\n - uses: actions/checkout@v4\n\n - uses: actions/setup-node@v4\n with:\n node-version: \"20\"\n\n - name: Install dependencies\n run: |\n cd frontend-app\n # Remove workspace refs (resolved by vite alias in dev, not needed for CI build)\n node -e \"const p=require('./package.json'); delete p.dependencies['@nexus/ai-bot']; delete p.dependencies['@nexus/bot-admin']; require('fs').writeFileSync('./package.json', JSON.stringify(p, null, 2));\"\n npm install --legacy-peer-deps\n\n - name: Build\n run: cd frontend-app && npx vite build\n\n # ══════════════════════════════════════\n # Packages — verify structure\n # ══════════════════════════════════════\n packages:\n runs-on: ubuntu-latest\n\n steps:\n - uses: actions/checkout@v4\n\n - uses: actions/setup-node@v4\n with:\n node-version: \"20\"\n\n - name: Verify @nexus/ai-bot structure\n run: |\n test -f packages/ai-bot/package.json\n test -f packages/ai-bot/src/index.ts\n test -f packages/ai-bot/src/BotProvider.tsx\n test -f packages/ai-bot/src/components/BotContainer.tsx\n test -f packages/ai-bot/src/components/VRMBotPlugin.ts\n test -f packages/ai-bot/src/hooks/useBotWebSocket.ts\n test -f packages/ai-bot/src/voice/BrowserVoiceProvider.ts\n echo \"@nexus/ai-bot structure OK\"\n\n - name: Verify @nexus/bot-admin structure\n run: |\n test -f packages/bot-admin/package.json\n test -f packages/bot-admin/src/index.ts\n test -f packages/bot-admin/src/BotManagePanel.tsx\n test -f packages/bot-admin/src/tabs/ScenesTab.tsx\n echo \"@nexus/bot-admin structure OK\"\n"}, "4f24ed35d18a7a6d701f5f5a69252999aefc68fa": {"t": 1785749328.1560736, "n": 5238, "h": "b79dd62729c2dbb6", "c": "name: CI\n\non:\n push:\n branches: [master, main]\n pull_request:\n branches: [master, main]\n\njobs:\n # ══════════════════════════════════════\n # Backend — Python lint + test\n # ══════════════════════════════════════\n backend:\n runs-on: ubuntu-latest\n defaults:\n run:\n working-directory: backend\n\n services:\n postgres:\n image: pgvector/pgvector:pg16\n env:\n POSTGRES_USER: postgres\n POSTGRES_PASSWORD: postgres\n POSTGRES_DB: ai_qa_test\n ports:\n - 5432:5432\n options: >-\n --health-cmd pg_isready\n --health-interval 10s\n --health-timeout 5s\n --health-retries 5\n\n redis:\n image: redis:7-alpine\n ports:\n - 6379:6379\n options: >-\n --health-cmd \"redis-cli ping\"\n --health-interval 10s\n --health-timeout 5s\n --health-retries 5\n\n steps:\n - uses: actions/checkout@v4\n\n - uses: actions/setup-python@v5\n with:\n python-version: \"3.11\"\n cache: pip\n\n - name: Install dependencies\n run: |\n pip install -r requirements.txt\n pip install ruff pytest pytest-asyncio httpx\n\n - name: Lint (ruff)\n run: ruff check app/ --select F --ignore F401,F841 || true\n\n - name: Unit tests\n run: python -m pytest tests/unit/ -v --tb=short -p no:pytest_ethereum -p no:web3\n\n - name: Type check (imports)\n run: |\n python -c \"\n from app.core.langgraph.state import QAState;\n from app.models import Base, User, Conversation, Message, BotMessage, BotScene, BotPreference;\n from app.services.bot.brain import think, BOT_PERSONAS;\n from app.services.bot.tools import get_tool_definitions;\n from app.services.bot.persistence import BotPersistenceService;\n from app.services.conversation import ConversationService;\n from app.services.llm import call_llm;\n from app.api.v1.router import api_router;\n print('All imports OK')\n \"\n\n - name: Run migrations\n env:\n DATABASE_URL: postgresql+asyncpg://postgres:postgres@localhost:5432/ai_qa_test\n REDIS_URL: redis://localhost:6379/0\n JWT_SECRET: test-secret\n DEEPSEEK_API_KEY: test\n run: alembic upgrade head\n\n - name: Test database schema\n env:\n DATABASE_URL: postgresql+asyncpg://postgres:postgres@localhost:5432/ai_qa_test\n REDIS_URL: redis://localhost:6379/0\n JWT_SECRET: test-secret\n DEEPSEEK_API_KEY: test\n run: |\n python << 'PYEOF'\n import asyncio\n from app.services.database import init_db, _session_factory\n async def check():\n await init_db()\n from app.services.database import _session_factory as sf\n async with sf() as s:\n from sqlalchemy import text\n r = await s.execute(text(\"SELECT tablename FROM pg_tables WHERE schemaname='public' ORDER BY tablename\"))\n tables = [row[0] for row in r]\n assert len(tables) >= 13, f\"Expected 13+ tables, got {len(tables)}: {tables}\"\n print(f\"OK: {len(tables)} tables\")\n asyncio.run(check())\n PYEOF\n\n # ══════════════════════════════════════\n # Frontend — lint + build\n # ══════════════════════════════════════\n frontend:\n runs-on: ubuntu-latest\n\n steps:\n - uses: actions/checkout@v4\n\n - uses: actions/setup-node@v4\n with:\n node-version: \"20\"\n\n - name: Install dependencies\n run: |\n cd frontend-app\n # Remove workspace refs (resolved by vite alias in dev, not needed for CI build)\n node -e \"const p=require('./package.json'); delete p.dependencies['@nexus/ai-bot']; delete p.dependencies['@nexus/bot-admin']; require('fs').writeFileSync('./package.json', JSON.stringify(p, null, 2));\"\n npm install --legacy-peer-deps\n\n - name: Build\n run: cd frontend-app && npx vite build\n\n # ══════════════════════════════════════\n # Packages — verify structure\n # ══════════════════════════════════════\n packages:\n runs-on: ubuntu-latest\n\n steps:\n - uses: actions/checkout@v4\n\n - uses: actions/setup-node@v4\n with:\n node-version: \"20\"\n\n - name: Verify @nexus/ai-bot structure\n run: |\n test -f packages/ai-bot/package.json\n test -f packages/ai-bot/src/index.ts\n test -f packages/ai-bot/src/BotProvider.tsx\n test -f packages/ai-bot/src/components/BotContainer.tsx\n test -f packages/ai-bot/src/components/VRMBotPlugin.ts\n test -f packages/ai-bot/src/hooks/useBotWebSocket.ts\n test -f packages/ai-bot/src/voice/BrowserVoiceProvider.ts\n echo \"@nexus/ai-bot structure OK\"\n\n - name: Verify @nexus/bot-admin structure\n run: |\n test -f packages/bot-admin/package.json\n test -f packages/bot-admin/src/index.ts\n test -f packages/bot-admin/src/BotManagePanel.tsx\n test -f packages/bot-admin/src/tabs/ScenesTab.tsx\n echo \"@nexus/bot-admin structure OK\"\n"}, "8e8cfa4d84104466fe465c5624fa4fec03e0a470": {"t": 1785749329.2537298, "n": 5123, "h": "af84981fdde70888", "c": "name: CI\n\non:\n push:\n branches: [master, main]\n pull_request:\n branches: [master, main]\n\njobs:\n # ══════════════════════════════════════\n # Backend — Python lint + test\n # ══════════════════════════════════════\n backend:\n runs-on: ubuntu-latest\n defaults:\n run:\n working-directory: backend\n\n services:\n postgres:\n image: pgvector/pgvector:pg16\n env:\n POSTGRES_USER: postgres\n POSTGRES_PASSWORD: postgres\n POSTGRES_DB: ai_qa_test\n ports:\n - 5432:5432\n options: >-\n --health-cmd pg_isready\n --health-interval 10s\n --health-timeout 5s\n --health-retries 5\n\n redis:\n image: redis:7-alpine\n ports:\n - 6379:6379\n options: >-\n --health-cmd \"redis-cli ping\"\n --health-interval 10s\n --health-timeout 5s\n --health-retries 5\n\n steps:\n - uses: actions/checkout@v4\n\n - uses: actions/setup-python@v5\n with:\n python-version: \"3.11\"\n cache: pip\n\n - name: Install dependencies\n run: |\n pip install -r requirements.txt\n pip install ruff pytest pytest-asyncio httpx\n\n - name: Lint (ruff)\n run: ruff check app/ --select F --ignore F401,F841 || true\n\n - name: Type check (imports)\n run: |\n python -c \"\n from app.core.langgraph.state import QAState;\n from app.models import Base, User, Conversation, Message, BotMessage, BotScene, BotPreference;\n from app.services.bot.brain import think, BOT_PERSONAS;\n from app.services.bot.tools import get_tool_definitions;\n from app.services.bot.persistence import BotPersistenceService;\n from app.services.conversation import ConversationService;\n from app.services.llm import call_llm;\n from app.api.v1.router import api_router;\n print('All imports OK')\n \"\n\n - name: Run migrations\n env:\n DATABASE_URL: postgresql+asyncpg://postgres:postgres@localhost:5432/ai_qa_test\n REDIS_URL: redis://localhost:6379/0\n JWT_SECRET: test-secret\n DEEPSEEK_API_KEY: test\n run: alembic upgrade head\n\n - name: Test database schema\n env:\n DATABASE_URL: postgresql+asyncpg://postgres:postgres@localhost:5432/ai_qa_test\n REDIS_URL: redis://localhost:6379/0\n JWT_SECRET: test-secret\n DEEPSEEK_API_KEY: test\n run: |\n python << 'PYEOF'\n import asyncio\n from app.services.database import init_db, _session_factory\n async def check():\n await init_db()\n from app.services.database import _session_factory as sf\n async with sf() as s:\n from sqlalchemy import text\n r = await s.execute(text(\"SELECT tablename FROM pg_tables WHERE schemaname='public' ORDER BY tablename\"))\n tables = [row[0] for row in r]\n assert len(tables) >= 13, f\"Expected 13+ tables, got {len(tables)}: {tables}\"\n print(f\"OK: {len(tables)} tables\")\n asyncio.run(check())\n PYEOF\n\n # ══════════════════════════════════════\n # Frontend — lint + build\n # ══════════════════════════════════════\n frontend:\n runs-on: ubuntu-latest\n\n steps:\n - uses: actions/checkout@v4\n\n - uses: actions/setup-node@v4\n with:\n node-version: \"20\"\n\n - name: Install dependencies\n run: |\n cd frontend-app\n # Remove workspace refs (resolved by vite alias in dev, not needed for CI build)\n node -e \"const p=require('./package.json'); delete p.dependencies['@nexus/ai-bot']; delete p.dependencies['@nexus/bot-admin']; require('fs').writeFileSync('./package.json', JSON.stringify(p, null, 2));\"\n npm install --legacy-peer-deps\n\n - name: Build\n run: cd frontend-app && npx vite build\n\n # ══════════════════════════════════════\n # Packages — verify structure\n # ══════════════════════════════════════\n packages:\n runs-on: ubuntu-latest\n\n steps:\n - uses: actions/checkout@v4\n\n - uses: actions/setup-node@v4\n with:\n node-version: \"20\"\n\n - name: Verify @nexus/ai-bot structure\n run: |\n test -f packages/ai-bot/package.json\n test -f packages/ai-bot/src/index.ts\n test -f packages/ai-bot/src/BotProvider.tsx\n test -f packages/ai-bot/src/components/BotContainer.tsx\n test -f packages/ai-bot/src/components/VRMBotPlugin.ts\n test -f packages/ai-bot/src/hooks/useBotWebSocket.ts\n test -f packages/ai-bot/src/voice/BrowserVoiceProvider.ts\n echo \"@nexus/ai-bot structure OK\"\n\n - name: Verify @nexus/bot-admin structure\n run: |\n test -f packages/bot-admin/package.json\n test -f packages/bot-admin/src/index.ts\n test -f packages/bot-admin/src/BotManagePanel.tsx\n test -f packages/bot-admin/src/tabs/ScenesTab.tsx\n echo \"@nexus/bot-admin structure OK\"\n"}, "92e327c64fa9799a64e83fd036c92ac4f3a98744": {"t": 1785749330.346835, "n": 4800, "h": "213ba7db7643f2f9", "c": "name: CI\n\non:\n push:\n branches: [master, main]\n pull_request:\n branches: [master, main]\n\njobs:\n # ══════════════════════════════════════\n # Backend — Python lint + test\n # ══════════════════════════════════════\n backend:\n runs-on: ubuntu-latest\n defaults:\n run:\n working-directory: backend\n\n services:\n postgres:\n image: pgvector/pgvector:pg16\n env:\n POSTGRES_USER: postgres\n POSTGRES_PASSWORD: postgres\n POSTGRES_DB: ai_qa_test\n ports:\n - 5432:5432\n options: >-\n --health-cmd pg_isready\n --health-interval 10s\n --health-timeout 5s\n --health-retries 5\n\n redis:\n image: redis:7-alpine\n ports:\n - 6379:6379\n options: >-\n --health-cmd \"redis-cli ping\"\n --health-interval 10s\n --health-timeout 5s\n --health-retries 5\n\n steps:\n - uses: actions/checkout@v4\n\n - uses: actions/setup-python@v5\n with:\n python-version: \"3.11\"\n cache: pip\n\n - name: Install dependencies\n run: |\n pip install -r requirements.txt\n pip install ruff pytest pytest-asyncio httpx\n\n - name: Lint (ruff)\n run: ruff check app/ --select F --ignore F401,F841 || true\n\n - name: Type check (imports)\n run: |\n python -c \"\n from app.core.langgraph.state import QAState;\n from app.models import Base, User, Conversation, Message, BotMessage, BotScene, BotPreference;\n from app.services.bot.brain import think, BOT_PERSONAS;\n from app.services.bot.tools import get_tool_definitions;\n from app.services.bot.persistence import BotPersistenceService;\n from app.services.conversation import ConversationService;\n from app.services.llm import call_llm;\n from app.api.v1.router import api_router;\n print('All imports OK')\n \"\n\n - name: Run migrations\n env:\n DATABASE_URL: postgresql+asyncpg://postgres:postgres@localhost:5432/ai_qa_test\n REDIS_URL: redis://localhost:6379/0\n JWT_SECRET: test-secret\n DEEPSEEK_API_KEY: test\n run: alembic upgrade head\n\n - name: Test database schema\n env:\n DATABASE_URL: postgresql+asyncpg://postgres:postgres@localhost:5432/ai_qa_test\n REDIS_URL: redis://localhost:6379/0\n JWT_SECRET: test-secret\n DEEPSEEK_API_KEY: test\n run: |\n python << 'PYEOF'\n import asyncio\n from app.services.database import init_db, _session_factory\n async def check():\n await init_db()\n from app.services.database import _session_factory as sf\n async with sf() as s:\n from sqlalchemy import text\n r = await s.execute(text(\"SELECT tablename FROM pg_tables WHERE schemaname='public' ORDER BY tablename\"))\n tables = [row[0] for row in r]\n assert len(tables) >= 13, f\"Expected 13+ tables, got {len(tables)}: {tables}\"\n print(f\"OK: {len(tables)} tables\")\n asyncio.run(check())\n PYEOF\n\n # ══════════════════════════════════════\n # Frontend — lint + build\n # ══════════════════════════════════════\n frontend:\n runs-on: ubuntu-latest\n\n steps:\n - uses: actions/checkout@v4\n\n - uses: actions/setup-node@v4\n with:\n node-version: \"20\"\n\n - name: Install dependencies\n run: cd frontend-app && npm install --legacy-peer-deps\n\n - name: Build\n run: cd frontend-app && npx vite build\n\n # ══════════════════════════════════════\n # Packages — verify structure\n # ══════════════════════════════════════\n packages:\n runs-on: ubuntu-latest\n\n steps:\n - uses: actions/checkout@v4\n\n - uses: actions/setup-node@v4\n with:\n node-version: \"20\"\n\n - name: Verify @nexus/ai-bot structure\n run: |\n test -f packages/ai-bot/package.json\n test -f packages/ai-bot/src/index.ts\n test -f packages/ai-bot/src/BotProvider.tsx\n test -f packages/ai-bot/src/components/BotContainer.tsx\n test -f packages/ai-bot/src/components/VRMBotPlugin.ts\n test -f packages/ai-bot/src/hooks/useBotWebSocket.ts\n test -f packages/ai-bot/src/voice/BrowserVoiceProvider.ts\n echo \"@nexus/ai-bot structure OK\"\n\n - name: Verify @nexus/bot-admin structure\n run: |\n test -f packages/bot-admin/package.json\n test -f packages/bot-admin/src/index.ts\n test -f packages/bot-admin/src/BotManagePanel.tsx\n test -f packages/bot-admin/src/tabs/ScenesTab.tsx\n echo \"@nexus/bot-admin structure OK\"\n"}, "f4c8a3b8a47f83fc8362b13453634f18bb5dbe58": {"t": 1785749331.5695252, "n": 4894, "h": "6cef88d1b8211f9c", "c": "name: CI\n\non:\n push:\n branches: [master, main]\n pull_request:\n branches: [master, main]\n\njobs:\n # ══════════════════════════════════════\n # Backend — Python lint + test\n # ══════════════════════════════════════\n backend:\n runs-on: ubuntu-latest\n defaults:\n run:\n working-directory: backend\n\n services:\n postgres:\n image: pgvector/pgvector:pg16\n env:\n POSTGRES_USER: postgres\n POSTGRES_PASSWORD: postgres\n POSTGRES_DB: ai_qa_test\n ports:\n - 5432:5432\n options: >-\n --health-cmd pg_isready\n --health-interval 10s\n --health-timeout 5s\n --health-retries 5\n\n redis:\n image: redis:7-alpine\n ports:\n - 6379:6379\n options: >-\n --health-cmd \"redis-cli ping\"\n --health-interval 10s\n --health-timeout 5s\n --health-retries 5\n\n steps:\n - uses: actions/checkout@v4\n\n - uses: actions/setup-python@v5\n with:\n python-version: \"3.11\"\n cache: pip\n\n - name: Install dependencies\n run: |\n pip install -r requirements.txt\n pip install ruff pytest pytest-asyncio httpx\n\n - name: Lint (ruff)\n run: ruff check app/ --select E,F,W --ignore E501\n\n - name: Type check (imports)\n run: |\n python -c \"\n from app.core.langgraph.state import QAState;\n from app.models import Base, User, Conversation, Message, BotMessage, BotScene, BotPreference;\n from app.services.bot.brain import think, BOT_PERSONAS;\n from app.services.bot.tools import get_tool_definitions;\n from app.services.bot.persistence import BotPersistenceService;\n from app.services.conversation import ConversationService;\n from app.services.llm import call_llm;\n from app.api.v1.router import api_router;\n print('All imports OK')\n \"\n\n - name: Run migrations\n env:\n DATABASE_URL: postgresql+asyncpg://postgres:postgres@localhost:5432/ai_qa_test\n REDIS_URL: redis://localhost:6379/0\n JWT_SECRET: test-secret\n DEEPSEEK_API_KEY: test\n run: alembic upgrade head\n\n - name: Test database schema\n env:\n DATABASE_URL: postgresql+asyncpg://postgres:postgres@localhost:5432/ai_qa_test\n REDIS_URL: redis://localhost:6379/0\n JWT_SECRET: test-secret\n DEEPSEEK_API_KEY: test\n run: |\n python << 'PYEOF'\n import asyncio\n from app.services.database import init_db, _session_factory\n async def check():\n await init_db()\n from app.services.database import _session_factory as sf\n async with sf() as s:\n from sqlalchemy import text\n r = await s.execute(text(\"SELECT tablename FROM pg_tables WHERE schemaname='public' ORDER BY tablename\"))\n tables = [row[0] for row in r]\n assert len(tables) >= 13, f\"Expected 13+ tables, got {len(tables)}: {tables}\"\n print(f\"OK: {len(tables)} tables\")\n asyncio.run(check())\n PYEOF\n\n # ══════════════════════════════════════\n # Frontend — lint + build\n # ══════════════════════════════════════\n frontend:\n runs-on: ubuntu-latest\n defaults:\n run:\n working-directory: frontend-app\n\n steps:\n - uses: actions/checkout@v4\n\n - uses: actions/setup-node@v4\n with:\n node-version: \"20\"\n cache: npm\n cache-dependency-path: frontend-app/package-lock.json\n\n - name: Install dependencies\n run: npm ci || npm install\n\n - name: Build\n run: npx vite build\n\n # ══════════════════════════════════════\n # Packages — verify structure\n # ══════════════════════════════════════\n packages:\n runs-on: ubuntu-latest\n\n steps:\n - uses: actions/checkout@v4\n\n - uses: actions/setup-node@v4\n with:\n node-version: \"20\"\n\n - name: Verify @nexus/ai-bot structure\n run: |\n test -f packages/ai-bot/package.json\n test -f packages/ai-bot/src/index.ts\n test -f packages/ai-bot/src/BotProvider.tsx\n test -f packages/ai-bot/src/components/BotContainer.tsx\n test -f packages/ai-bot/src/components/VRMBotPlugin.ts\n test -f packages/ai-bot/src/hooks/useBotWebSocket.ts\n test -f packages/ai-bot/src/voice/BrowserVoiceProvider.ts\n echo \"@nexus/ai-bot structure OK\"\n\n - name: Verify @nexus/bot-admin structure\n run: |\n test -f packages/bot-admin/package.json\n test -f packages/bot-admin/src/index.ts\n test -f packages/bot-admin/src/BotManagePanel.tsx\n test -f packages/bot-admin/src/tabs/ScenesTab.tsx\n echo \"@nexus/bot-admin structure OK\"\n"}, "f6fb4dba2a6110c2e6c1cf37feb60a47f315d5f7": {"t": 1785749332.6966624, "n": 4862, "h": "85e13f25b8c78898", "c": "name: CI\n\non:\n push:\n branches: [master, main]\n pull_request:\n branches: [master, main]\n\njobs:\n # ══════════════════════════════════════\n # Backend — Python lint + test\n # ══════════════════════════════════════\n backend:\n runs-on: ubuntu-latest\n defaults:\n run:\n working-directory: backend\n\n services:\n postgres:\n image: pgvector/pgvector:pg16\n env:\n POSTGRES_USER: postgres\n POSTGRES_PASSWORD: postgres\n POSTGRES_DB: ai_qa_test\n ports:\n - 5432:5432\n options: >-\n --health-cmd pg_isready\n --health-interval 10s\n --health-timeout 5s\n --health-retries 5\n\n redis:\n image: redis:7-alpine\n ports:\n - 6379:6379\n options: >-\n --health-cmd \"redis-cli ping\"\n --health-interval 10s\n --health-timeout 5s\n --health-retries 5\n\n steps:\n - uses: actions/checkout@v4\n\n - uses: actions/setup-python@v5\n with:\n python-version: \"3.11\"\n cache: pip\n\n - name: Install dependencies\n run: |\n pip install -r requirements.txt\n pip install ruff pytest pytest-asyncio httpx\n\n - name: Lint (ruff)\n run: ruff check app/ --select E,F,W --ignore E501\n\n - name: Type check (imports)\n run: python -c \"\n from app.core.langgraph.state import QAState;\n from app.models import Base, User, Conversation, Message, BotMessage, BotScene, BotPreference;\n from app.services.bot.brain import think, BOT_PERSONAS;\n from app.services.bot.tools import get_tool_definitions;\n from app.services.bot.persistence import BotPersistenceService;\n from app.services.conversation import ConversationService;\n from app.services.llm import call_llm;\n from app.api.v1.router import api_router;\n print('All imports OK')\n \"\n\n - name: Run migrations\n env:\n DATABASE_URL: postgresql+asyncpg://postgres:postgres@localhost:5432/ai_qa_test\n REDIS_URL: redis://localhost:6379/0\n JWT_SECRET: test-secret\n DEEPSEEK_API_KEY: test\n run: alembic upgrade head\n\n - name: Test database schema\n env:\n DATABASE_URL: postgresql+asyncpg://postgres:postgres@localhost:5432/ai_qa_test\n REDIS_URL: redis://localhost:6379/0\n JWT_SECRET: test-secret\n DEEPSEEK_API_KEY: test\n run: python -c \"\n import asyncio\n from app.services.database import init_db, _session_factory\n async def check():\n await init_db()\n from app.services.database import _session_factory as sf\n async with sf() as s:\n from sqlalchemy import text\n r = await s.execute(text(\\\"SELECT tablename FROM pg_tables WHERE schemaname='public' ORDER BY tablename\\\"))\n tables = [row[0] for row in r]\n assert len(tables) >= 13, f'Expected 13+ tables, got {len(tables)}: {tables}'\n print(f'OK: {len(tables)} tables')\n asyncio.run(check())\n \"\n\n # ══════════════════════════════════════\n # Frontend — lint + build\n # ══════════════════════════════════════\n frontend:\n runs-on: ubuntu-latest\n defaults:\n run:\n working-directory: frontend-app\n\n steps:\n - uses: actions/checkout@v4\n\n - uses: actions/setup-node@v4\n with:\n node-version: \"20\"\n cache: npm\n cache-dependency-path: frontend-app/package-lock.json\n\n - name: Install dependencies\n run: npm ci || npm install\n\n - name: Build\n run: npx vite build\n\n # ══════════════════════════════════════\n # Packages — verify structure\n # ══════════════════════════════════════\n packages:\n runs-on: ubuntu-latest\n\n steps:\n - uses: actions/checkout@v4\n\n - uses: actions/setup-node@v4\n with:\n node-version: \"20\"\n\n - name: Verify @nexus/ai-bot structure\n run: |\n test -f packages/ai-bot/package.json\n test -f packages/ai-bot/src/index.ts\n test -f packages/ai-bot/src/BotProvider.tsx\n test -f packages/ai-bot/src/components/BotContainer.tsx\n test -f packages/ai-bot/src/components/VRMBotPlugin.ts\n test -f packages/ai-bot/src/hooks/useBotWebSocket.ts\n test -f packages/ai-bot/src/voice/BrowserVoiceProvider.ts\n echo \"@nexus/ai-bot structure OK\"\n\n - name: Verify @nexus/bot-admin structure\n run: |\n test -f packages/bot-admin/package.json\n test -f packages/bot-admin/src/index.ts\n test -f packages/bot-admin/src/BotManagePanel.tsx\n test -f packages/bot-admin/src/tabs/ScenesTab.tsx\n echo \"@nexus/bot-admin structure OK\"\n"}}, "frontend-app/src/pages/Settings/index.tsx": {"2916bcc3690cc891de80d898da649cc67ba3cfb9": {"t": 1785746448.7552412, "n": 21077, "h": "0660a291b1fe331c", "c": "import { useState } from 'react';\nimport { useBotStore } from '@nexus/ai-bot';\nimport { useThemeStore } from '../../store/theme';\nimport { Button } from '../../components/ui/Button';\nimport { Input } from '../../components/ui/Input';\nimport { toast } from '../../components/ui/Toast';\nimport { Bot, Cpu, Palette, Globe, Shield, User, Mic } from 'lucide-react';\n\nconst LLM_MODELS = [\n { value: 'deepseek/deepseek-chat', label: 'DeepSeek Chat', desc: 'Fast & cheap' },\n { value: 'openai/gpt-4o', label: 'GPT-4o', desc: 'Popular' },\n { value: 'anthropic/claude-sonnet-4-20250514', label: 'Claude Sonnet', desc: 'Best quality' },\n { value: 'ollama/llama3', label: 'Ollama Llama3', desc: 'Free, local' },\n { value: 'ollama/qwen2.5', label: 'Ollama Qwen2.5', desc: 'Free, local, Chinese' },\n];\n\nconst LANGUAGES = [\n { value: 'auto', label: 'Auto (match user)' },\n { value: 'en', label: 'English' },\n { value: 'zh', label: '中文' },\n { value: 'ja', label: '日本語' },\n];\n\nconst DEFAULT_PERSONAS = [\n { id: 'clawford', name: 'Clawford', emoji: '🦀', role: 'Senior Data Analyst', personality: 'Professional yet witty, data-driven with a pinch of humor', expertise: 'Financial analysis, business intelligence, KPI tracking', greeting: \"Hello! I'm Clawford, your enterprise data analyst 🦀 Ready to crunch some numbers?\" },\n { id: 'nexus', name: 'Nexus', emoji: '⚡', role: 'System Operations Engineer', personality: 'Strictly professional, concise, precise', expertise: 'System monitoring, DevOps, performance optimization', greeting: \"Nexus online. All systems nominal.\" },\n { id: 'buddy', name: 'Buddy', emoji: '🎉', role: 'General Assistant', personality: 'Casual, talkative, friendly, lots of emoji', expertise: 'General Q&A, onboarding, feature explanation', greeting: \"Hey there! What's up? 🎉\" },\n];\n\nfunction loadPersonas() {\n const custom = localStorage.getItem('custom_personas');\n const customs = custom ? JSON.parse(custom) : [];\n // Merge: custom overrides default if same id, otherwise append\n const merged = [...DEFAULT_PERSONAS];\n for (const c of customs) {\n const idx = merged.findIndex(m => m.id === c.id);\n if (idx >= 0) merged[idx] = { ...merged[idx], ...c };\n else merged.push(c);\n }\n return merged;\n}\n\nfunction saveCustomPersona(p: { id: string; name: string; emoji: string; personality: string; greeting: string }) {\n const custom = localStorage.getItem('custom_personas');\n const customs = custom ? JSON.parse(custom) : [];\n const existing = customs.findIndex((c: any) => c.id === p.id);\n if (existing >= 0) customs[existing] = p;\n else customs.push(p);\n localStorage.setItem('custom_personas', JSON.stringify(customs));\n}\n\nfunction deleteCustomPersona(id: string) {\n const custom = localStorage.getItem('custom_personas');\n const customs = custom ? JSON.parse(custom) : [];\n localStorage.setItem('custom_personas', JSON.stringify(customs.filter((c: any) => c.id !== id)));\n}\n\nexport default function SettingsPage() {\n const { enabled: botEnabled, setEnabled: setBotEnabled, size: botSize, setSize: setBotSize } = useBotStore();\n const { theme } = useThemeStore();\n\n const [model, setModel] = useState(() => localStorage.getItem('settings_model') || 'deepseek/deepseek-chat');\n const [apiKey, setApiKey] = useState(() => localStorage.getItem('settings_api_key') || '');\n const [language, setLanguage] = useState(() => localStorage.getItem('settings_language') || 'auto');\n const [persona, setPersona] = useState(() => localStorage.getItem('settings_persona') || 'clawford');\n const [personas, setPersonas] = useState(loadPersonas);\n const [editingPersona, setEditingPersona] = useState<any>(null);\n const [showPersonaEditor, setShowPersonaEditor] = useState(false);\n\n const handlePersonaChange = (id: string) => {\n setPersona(id);\n localStorage.setItem('settings_persona', id);\n const ws = (window as any).__botWs;\n if (ws?.readyState === WebSocket.OPEN) {\n ws.send(JSON.stringify({ type: 'persona_change', persona: id }));\n }\n toast(`Persona: ${personas.find(p => p.id === id)?.name}`, 'success');\n };\n\n const handleSavePersona = () => {\n if (!editingPersona?.name) return;\n const p = { ...editingPersona, id: editingPersona.id || `custom_${Date.now()}` };\n saveCustomPersona(p);\n setPersonas(loadPersonas());\n setShowPersonaEditor(false);\n setEditingPersona(null);\n // Also send to backend to register\n const ws = (window as any).__botWs;\n if (ws?.readyState === WebSocket.OPEN) {\n ws.send(JSON.stringify({ type: 'register_persona', persona: p }));\n }\n toast(`Persona \"${p.name}\" saved`, 'success');\n };\n\n const handleDeletePersona = (id: string) => {\n if (DEFAULT_PERSONAS.find(p => p.id === id)) { toast('Cannot delete built-in persona', 'error'); return; }\n deleteCustomPersona(id);\n setPersonas(loadPersonas());\n if (persona === id) handlePersonaChange('nexus');\n toast('Persona deleted', 'info');\n };\n\n const handleSaveModel = () => {\n localStorage.setItem('settings_model', model);\n if (apiKey) localStorage.setItem('settings_api_key', apiKey);\n else localStorage.removeItem('settings_api_key');\n toast('Model & API key saved', 'success');\n };\n\n const handleLanguage = (lang: string) => {\n setLanguage(lang);\n localStorage.setItem('settings_language', lang);\n toast(`Language: ${lang}`, 'success');\n };\n\n return (\n <div style={{ overflowY: 'auto', padding: '24px 32px', display: 'flex', flexDirection: 'column', gap: 24, height: '100%', maxWidth: 700 }}>\n <div>\n <h1 className=\"font-display\" style={{ fontSize: 44, letterSpacing: 6, color: 'var(--ink)', lineHeight: 1 }}>\n SETTINGS\n </h1>\n <p className=\"font-mono\" style={{ fontSize: 10, color: 'var(--dim)', letterSpacing: 1, marginTop: 4 }}>\n // SYSTEM CONFIGURATION\n </p>\n </div>\n\n {/* LLM Model + API Key — has save button */}\n <SettingSection icon={Cpu} title=\"LLM MODEL\">\n <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>\n {LLM_MODELS.map(m => (\n <label key={m.value} style={{\n display: 'flex', alignItems: 'center', gap: 10, padding: '10px 14px',\n border: `2px solid ${model === m.value ? 'var(--orange)' : 'var(--line)'}`,\n background: model === m.value ? 'rgba(212, 82, 26, 0.06)' : 'transparent',\n cursor: 'pointer', transition: 'all 0.2s',\n }}>\n <input type=\"radio\" name=\"model\" value={m.value} checked={model === m.value}\n onChange={() => setModel(m.value)} style={{ accentColor: 'var(--orange)' }} />\n <div style={{ flex: 1 }}>\n <div style={{ fontSize: 13, fontWeight: 500 }}>{m.label}</div>\n <div className=\"font-mono\" style={{ fontSize: 10, color: 'var(--dim)' }}>{m.desc}</div>\n </div>\n </label>\n ))}\n </div>\n\n <div style={{ marginTop: 14 }}>\n <div className=\"font-mono\" style={{ fontSize: 9, color: 'var(--orange)', letterSpacing: 2, marginBottom: 6, textTransform: 'uppercase', display: 'flex', alignItems: 'center', gap: 6 }}>\n <Shield size={10} /> API Key (optional)\n </div>\n <div style={{ display: 'flex', gap: 8 }}>\n <Input type=\"password\" placeholder=\"sk-... (leave empty for server default)\"\n value={apiKey} onChange={e => setApiKey(e.target.value)} style={{ flex: 1 }} />\n <Button onClick={handleSaveModel} size=\"sm\">SAVE</Button>\n </div>\n </div>\n </SettingSection>\n\n {/* Language — instant save on click */}\n <SettingSection icon={Globe} title=\"LANGUAGE\">\n <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>\n {LANGUAGES.map(l => (\n <button key={l.value} onClick={() => handleLanguage(l.value)} className=\"font-mono\"\n style={{\n padding: '8px 16px', border: `2px solid ${language === l.value ? 'var(--orange)' : 'var(--line)'}`,\n background: language === l.value ? 'rgba(212, 82, 26, 0.06)' : 'var(--cream)',\n color: language === l.value ? 'var(--orange)' : 'var(--ink)',\n fontSize: 12, cursor: 'pointer', transition: 'all 0.2s',\n }}>\n {l.label}\n </button>\n ))}\n </div>\n </SettingSection>\n\n {/* Theme — instant */}\n <SettingSection icon={Palette} title=\"THEME\">\n <div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>\n <button className=\"font-mono\" style={{\n padding: '8px 16px', border: '2px solid var(--orange)',\n background: 'rgba(212, 82, 26, 0.06)', color: 'var(--orange)',\n fontSize: 12, cursor: 'default', textTransform: 'uppercase',\n }}>\n NEXUS WARM\n </button>\n <span className=\"font-mono\" style={{ fontSize: 10, color: 'var(--dim)' }}>\n Dark mode coming soon\n </span>\n </div>\n </SettingSection>\n\n {/* Bot Persona */}\n <SettingSection icon={User} title=\"BOT PERSONA\">\n <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>\n {personas.map(p => (\n <div key={p.id} style={{\n display: 'flex', alignItems: 'center', gap: 10, padding: '10px 14px',\n border: `2px solid ${persona === p.id ? 'var(--orange)' : 'var(--line)'}`,\n background: persona === p.id ? 'rgba(212, 82, 26, 0.06)' : 'transparent',\n cursor: 'pointer', transition: 'all 0.2s',\n }} onClick={() => handlePersonaChange(p.id)}>\n <input type=\"radio\" name=\"persona\" value={p.id} checked={persona === p.id}\n onChange={() => {}} style={{ accentColor: 'var(--orange)' }} />\n <div style={{ flex: 1 }}>\n <div style={{ fontSize: 13, fontWeight: 500 }}>{p.emoji} {p.name}</div>\n {p.role && <div className=\"font-mono\" style={{ fontSize: 10, color: 'var(--orange)' }}>{p.role}</div>}\n <div className=\"font-mono\" style={{ fontSize: 10, color: 'var(--dim)' }}>{p.personality}</div>\n </div>\n <div style={{ display: 'flex', gap: 4 }}>\n <button onClick={e => { e.stopPropagation(); setEditingPersona({ ...p }); setShowPersonaEditor(true); }}\n className=\"font-mono\" style={{ background: 'none', border: '1px solid var(--line)', padding: '2px 6px', fontSize: 9, cursor: 'pointer', color: 'var(--mid)' }}>\n EDIT\n </button>\n {!DEFAULT_PERSONAS.find(d => d.id === p.id) && (\n <button onClick={e => { e.stopPropagation(); handleDeletePersona(p.id); }}\n className=\"font-mono\" style={{ background: 'none', border: '1px solid var(--error)', padding: '2px 6px', fontSize: 9, cursor: 'pointer', color: 'var(--error)' }}>\n DEL\n </button>\n )}\n </div>\n </div>\n ))}\n </div>\n <button onClick={() => { setEditingPersona({ id: '', name: '', emoji: '🤖', personality: '', greeting: '' }); setShowPersonaEditor(true); }}\n className=\"font-mono\" style={{\n marginTop: 10, padding: '8px 14px', border: '2px dashed var(--line)',\n background: 'transparent', color: 'var(--orange)', fontSize: 11,\n cursor: 'pointer', width: '100%', textAlign: 'center',\n }}>\n + CREATE CUSTOM PERSONA\n </button>\n\n {/* Persona Editor */}\n {showPersonaEditor && editingPersona && (\n <div style={{\n marginTop: 12, padding: 14, border: '2px solid var(--orange)',\n background: 'rgba(212, 82, 26, 0.03)', display: 'flex', flexDirection: 'column', gap: 10,\n }}>\n <div className=\"font-display\" style={{ fontSize: 14, letterSpacing: 2 }}>\n {editingPersona.id ? 'EDIT PERSONA' : 'NEW PERSONA'}\n </div>\n <div style={{ display: 'flex', gap: 8 }}>\n <div style={{ flex: 0 }}>\n <div className=\"font-mono\" style={{ fontSize: 9, color: 'var(--dim)', marginBottom: 4 }}>EMOJI</div>\n <Input value={editingPersona.emoji} onChange={e => setEditingPersona({ ...editingPersona, emoji: e.target.value })}\n style={{ width: 50, textAlign: 'center', fontSize: 20 }} />\n </div>\n <div style={{ flex: 1 }}>\n <div className=\"font-mono\" style={{ fontSize: 9, color: 'var(--dim)', marginBottom: 4 }}>NAME</div>\n <Input value={editingPersona.name} onChange={e => setEditingPersona({ ...editingPersona, name: e.target.value })}\n placeholder=\"e.g. Clawford\" />\n </div>\n </div>\n <div>\n <div className=\"font-mono\" style={{ fontSize: 9, color: 'var(--dim)', marginBottom: 4 }}>ROLE / IDENTITY</div>\n <Input value={editingPersona.role || ''} onChange={e => setEditingPersona({ ...editingPersona, role: e.target.value })}\n placeholder=\"e.g. Senior Data Analyst, HR Manager, DevOps Engineer\" />\n </div>\n <div>\n <div className=\"font-mono\" style={{ fontSize: 9, color: 'var(--dim)', marginBottom: 4 }}>PERSONALITY</div>\n <Input value={editingPersona.personality} onChange={e => setEditingPersona({ ...editingPersona, personality: e.target.value })}\n placeholder=\"e.g. Professional yet witty, data-driven\" />\n </div>\n <div>\n <div className=\"font-mono\" style={{ fontSize: 9, color: 'var(--dim)', marginBottom: 4 }}>EXPERTISE</div>\n <Input value={editingPersona.expertise || ''} onChange={e => setEditingPersona({ ...editingPersona, expertise: e.target.value })}\n placeholder=\"e.g. Financial analysis, KPI tracking, budget planning\" />\n </div>\n <div>\n <div className=\"font-mono\" style={{ fontSize: 9, color: 'var(--dim)', marginBottom: 4 }}>GREETING</div>\n <Input value={editingPersona.greeting} onChange={e => setEditingPersona({ ...editingPersona, greeting: e.target.value })}\n placeholder=\"e.g. Hello! I'm Clawford 🦀\" />\n </div>\n <div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end' }}>\n <Button variant=\"outline\" size=\"sm\" onClick={() => { setShowPersonaEditor(false); setEditingPersona(null); }}>CANCEL</Button>\n <Button size=\"sm\" onClick={handleSavePersona}>SAVE</Button>\n </div>\n </div>\n )}\n </SettingSection>\n\n {/* Bot — instant toggle + slider */}\n <SettingSection icon={Bot} title=\"3D BOT\">\n <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>\n <label style={{ display: 'flex', alignItems: 'center', gap: 8, cursor: 'pointer' }}>\n <input type=\"checkbox\" checked={botEnabled} onChange={e => setBotEnabled(e.target.checked)}\n style={{ width: 18, height: 18, accentColor: 'var(--orange)' }} />\n <span style={{ fontSize: 13 }}>{botEnabled ? 'Enabled' : 'Disabled'}</span>\n </label>\n {botEnabled && (\n <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>\n <span className=\"font-mono\" style={{ fontSize: 10, color: 'var(--dim)', minWidth: 30 }}>Size</span>\n <input type=\"range\" min={80} max={260} value={botSize}\n onChange={e => {\n const v = Number(e.target.value);\n setBotSize(v);\n // Debounced save to DB\n clearTimeout((window as any).__botSizeTimer);\n (window as any).__botSizeTimer = setTimeout(() => {\n const token = localStorage.getItem('token');\n if (token) fetch('/api/v1/bot/preferences', { method: 'PUT', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` }, body: JSON.stringify({ bot_size: v }) }).catch(() => {});\n }, 500);\n }}\n style={{ flex: 1, maxWidth: 200, accentColor: 'var(--orange)' }} />\n <span className=\"font-mono\" style={{ fontSize: 10, color: 'var(--dim)', minWidth: 40 }}>{botSize}px</span>\n </div>\n )}\n </div>\n </SettingSection>\n\n {/* Voice */}\n <SettingSection icon={Mic} title=\"VOICE\">\n <VoiceSettings />\n </SettingSection>\n </div>\n );\n}\n\nfunction VoiceSettings() {\n const [config, setConfig] = useState(() => {\n try {\n const saved = localStorage.getItem('voice_config');\n return saved ? JSON.parse(saved) : { enabled: false, ttsProvider: 'browser', lang: 'zh-CN', rate: 1.0, autoSpeak: true };\n } catch { return { enabled: false, ttsProvider: 'browser', lang: 'zh-CN', rate: 1.0, autoSpeak: true }; }\n });\n\n const save = (updates: Record<string, any>) => {\n const next = { ...config, ...updates };\n setConfig(next);\n localStorage.setItem('voice_config', JSON.stringify(next));\n window.dispatchEvent(new Event('voice_config_changed'));\n };\n\n const hasBrowserSTT = !!(window as any).SpeechRecognition || !!(window as any).webkitSpeechRecognition;\n\n return (\n <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>\n <label style={{ display: 'flex', alignItems: 'center', gap: 8, cursor: 'pointer' }}>\n <input type=\"checkbox\" checked={config.enabled} onChange={e => save({ enabled: e.target.checked })}\n style={{ width: 18, height: 18, accentColor: 'var(--orange)' }} />\n <span style={{ fontSize: 13 }}>{config.enabled ? 'Voice Enabled' : 'Voice Disabled'}</span>\n {!hasBrowserSTT && <span className=\"font-mono\" style={{ fontSize: 9, color: '#c0392b' }}>(STT not supported in this browser)</span>}\n </label>\n\n {config.enabled && (\n <>\n <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>\n <span className=\"font-mono\" style={{ fontSize: 10, color: 'var(--dim)', minWidth: 80 }}>TTS Provider</span>\n <select value={config.ttsProvider} onChange={e => save({ ttsProvider: e.target.value })}\n className=\"font-mono\" style={{ fontSize: 11, padding: '4px 8px', border: '1.5px solid var(--line)', background: 'var(--cream)' }}>\n <option value=\"browser\">Browser Native (Free)</option>\n <option value=\"edge\">Edge TTS (Free, High Quality)</option>\n <option value=\"openai\">OpenAI TTS (Paid, Best)</option>\n </select>\n </div>\n\n <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>\n <span className=\"font-mono\" style={{ fontSize: 10, color: 'var(--dim)', minWidth: 80 }}>Language</span>\n <select value={config.lang} onChange={e => save({ lang: e.target.value })}\n className=\"font-mono\" style={{ fontSize: 11, padding: '4px 8px', border: '1.5px solid var(--line)', background: 'var(--cream)' }}>\n <option value=\"zh-CN\">Chinese (zh-CN)</option>\n <option value=\"en-US\">English (en-US)</option>\n <option value=\"ja-JP\">Japanese (ja-JP)</option>\n <option value=\"ko-KR\">Korean (ko-KR)</option>\n <option value=\"de-DE\">German (de-DE)</option>\n <option value=\"fr-FR\">French (fr-FR)</option>\n </select>\n </div>\n\n <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>\n <span className=\"font-mono\" style={{ fontSize: 10, color: 'var(--dim)', minWidth: 80 }}>Speed</span>\n <input type=\"range\" min={0.5} max={2.0} step={0.1} value={config.rate}\n onChange={e => save({ rate: parseFloat(e.target.value) })}\n style={{ flex: 1, maxWidth: 150, accentColor: 'var(--orange)' }} />\n <span className=\"font-mono\" style={{ fontSize: 10, color: 'var(--dim)' }}>{config.rate}x</span>\n </div>\n\n <label style={{ display: 'flex', alignItems: 'center', gap: 8, cursor: 'pointer' }}>\n <input type=\"checkbox\" checked={config.autoSpeak} onChange={e => save({ autoSpeak: e.target.checked })}\n style={{ width: 16, height: 16, accentColor: 'var(--orange)' }} />\n <span className=\"font-mono\" style={{ fontSize: 11, color: 'var(--mid)' }}>Auto-speak bot responses</span>\n </label>\n </>\n )}\n </div>\n );\n}\n\nfunction SettingSection({ icon: Icon, title, children }: {\n icon: any; title: string; children: React.ReactNode;\n}) {\n return (\n <div className=\"animate-card-in\" style={{ border: '2px solid var(--ink)', padding: '18px 20px', background: 'var(--cream)' }}>\n <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 14 }}>\n <Icon size={16} color=\"var(--orange)\" />\n <span className=\"font-display\" style={{ fontSize: 16, letterSpacing: 3 }}>{title}</span>\n </div>\n {children}\n </div>\n );\n}\n"}, "33fcc0037fcd6eecfe0b27c50d6cf46096740afc": {"t": 1785749327.5863101, "n": 21077, "h": "0660a291b1fe331c", "c": "import { useState } from 'react';\nimport { useBotStore } from '@nexus/ai-bot';\nimport { useThemeStore } from '../../store/theme';\nimport { Button } from '../../components/ui/Button';\nimport { Input } from '../../components/ui/Input';\nimport { toast } from '../../components/ui/Toast';\nimport { Bot, Cpu, Palette, Globe, Shield, User, Mic } from 'lucide-react';\n\nconst LLM_MODELS = [\n { value: 'deepseek/deepseek-chat', label: 'DeepSeek Chat', desc: 'Fast & cheap' },\n { value: 'openai/gpt-4o', label: 'GPT-4o', desc: 'Popular' },\n { value: 'anthropic/claude-sonnet-4-20250514', label: 'Claude Sonnet', desc: 'Best quality' },\n { value: 'ollama/llama3', label: 'Ollama Llama3', desc: 'Free, local' },\n { value: 'ollama/qwen2.5', label: 'Ollama Qwen2.5', desc: 'Free, local, Chinese' },\n];\n\nconst LANGUAGES = [\n { value: 'auto', label: 'Auto (match user)' },\n { value: 'en', label: 'English' },\n { value: 'zh', label: '中文' },\n { value: 'ja', label: '日本語' },\n];\n\nconst DEFAULT_PERSONAS = [\n { id: 'clawford', name: 'Clawford', emoji: '🦀', role: 'Senior Data Analyst', personality: 'Professional yet witty, data-driven with a pinch of humor', expertise: 'Financial analysis, business intelligence, KPI tracking', greeting: \"Hello! I'm Clawford, your enterprise data analyst 🦀 Ready to crunch some numbers?\" },\n { id: 'nexus', name: 'Nexus', emoji: '⚡', role: 'System Operations Engineer', personality: 'Strictly professional, concise, precise', expertise: 'System monitoring, DevOps, performance optimization', greeting: \"Nexus online. All systems nominal.\" },\n { id: 'buddy', name: 'Buddy', emoji: '🎉', role: 'General Assistant', personality: 'Casual, talkative, friendly, lots of emoji', expertise: 'General Q&A, onboarding, feature explanation', greeting: \"Hey there! What's up? 🎉\" },\n];\n\nfunction loadPersonas() {\n const custom = localStorage.getItem('custom_personas');\n const customs = custom ? JSON.parse(custom) : [];\n // Merge: custom overrides default if same id, otherwise append\n const merged = [...DEFAULT_PERSONAS];\n for (const c of customs) {\n const idx = merged.findIndex(m => m.id === c.id);\n if (idx >= 0) merged[idx] = { ...merged[idx], ...c };\n else merged.push(c);\n }\n return merged;\n}\n\nfunction saveCustomPersona(p: { id: string; name: string; emoji: string; personality: string; greeting: string }) {\n const custom = localStorage.getItem('custom_personas');\n const customs = custom ? JSON.parse(custom) : [];\n const existing = customs.findIndex((c: any) => c.id === p.id);\n if (existing >= 0) customs[existing] = p;\n else customs.push(p);\n localStorage.setItem('custom_personas', JSON.stringify(customs));\n}\n\nfunction deleteCustomPersona(id: string) {\n const custom = localStorage.getItem('custom_personas');\n const customs = custom ? JSON.parse(custom) : [];\n localStorage.setItem('custom_personas', JSON.stringify(customs.filter((c: any) => c.id !== id)));\n}\n\nexport default function SettingsPage() {\n const { enabled: botEnabled, setEnabled: setBotEnabled, size: botSize, setSize: setBotSize } = useBotStore();\n const { theme } = useThemeStore();\n\n const [model, setModel] = useState(() => localStorage.getItem('settings_model') || 'deepseek/deepseek-chat');\n const [apiKey, setApiKey] = useState(() => localStorage.getItem('settings_api_key') || '');\n const [language, setLanguage] = useState(() => localStorage.getItem('settings_language') || 'auto');\n const [persona, setPersona] = useState(() => localStorage.getItem('settings_persona') || 'clawford');\n const [personas, setPersonas] = useState(loadPersonas);\n const [editingPersona, setEditingPersona] = useState<any>(null);\n const [showPersonaEditor, setShowPersonaEditor] = useState(false);\n\n const handlePersonaChange = (id: string) => {\n setPersona(id);\n localStorage.setItem('settings_persona', id);\n const ws = (window as any).__botWs;\n if (ws?.readyState === WebSocket.OPEN) {\n ws.send(JSON.stringify({ type: 'persona_change', persona: id }));\n }\n toast(`Persona: ${personas.find(p => p.id === id)?.name}`, 'success');\n };\n\n const handleSavePersona = () => {\n if (!editingPersona?.name) return;\n const p = { ...editingPersona, id: editingPersona.id || `custom_${Date.now()}` };\n saveCustomPersona(p);\n setPersonas(loadPersonas());\n setShowPersonaEditor(false);\n setEditingPersona(null);\n // Also send to backend to register\n const ws = (window as any).__botWs;\n if (ws?.readyState === WebSocket.OPEN) {\n ws.send(JSON.stringify({ type: 'register_persona', persona: p }));\n }\n toast(`Persona \"${p.name}\" saved`, 'success');\n };\n\n const handleDeletePersona = (id: string) => {\n if (DEFAULT_PERSONAS.find(p => p.id === id)) { toast('Cannot delete built-in persona', 'error'); return; }\n deleteCustomPersona(id);\n setPersonas(loadPersonas());\n if (persona === id) handlePersonaChange('nexus');\n toast('Persona deleted', 'info');\n };\n\n const handleSaveModel = () => {\n localStorage.setItem('settings_model', model);\n if (apiKey) localStorage.setItem('settings_api_key', apiKey);\n else localStorage.removeItem('settings_api_key');\n toast('Model & API key saved', 'success');\n };\n\n const handleLanguage = (lang: string) => {\n setLanguage(lang);\n localStorage.setItem('settings_language', lang);\n toast(`Language: ${lang}`, 'success');\n };\n\n return (\n <div style={{ overflowY: 'auto', padding: '24px 32px', display: 'flex', flexDirection: 'column', gap: 24, height: '100%', maxWidth: 700 }}>\n <div>\n <h1 className=\"font-display\" style={{ fontSize: 44, letterSpacing: 6, color: 'var(--ink)', lineHeight: 1 }}>\n SETTINGS\n </h1>\n <p className=\"font-mono\" style={{ fontSize: 10, color: 'var(--dim)', letterSpacing: 1, marginTop: 4 }}>\n // SYSTEM CONFIGURATION\n </p>\n </div>\n\n {/* LLM Model + API Key — has save button */}\n <SettingSection icon={Cpu} title=\"LLM MODEL\">\n <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>\n {LLM_MODELS.map(m => (\n <label key={m.value} style={{\n display: 'flex', alignItems: 'center', gap: 10, padding: '10px 14px',\n border: `2px solid ${model === m.value ? 'var(--orange)' : 'var(--line)'}`,\n background: model === m.value ? 'rgba(212, 82, 26, 0.06)' : 'transparent',\n cursor: 'pointer', transition: 'all 0.2s',\n }}>\n <input type=\"radio\" name=\"model\" value={m.value} checked={model === m.value}\n onChange={() => setModel(m.value)} style={{ accentColor: 'var(--orange)' }} />\n <div style={{ flex: 1 }}>\n <div style={{ fontSize: 13, fontWeight: 500 }}>{m.label}</div>\n <div className=\"font-mono\" style={{ fontSize: 10, color: 'var(--dim)' }}>{m.desc}</div>\n </div>\n </label>\n ))}\n </div>\n\n <div style={{ marginTop: 14 }}>\n <div className=\"font-mono\" style={{ fontSize: 9, color: 'var(--orange)', letterSpacing: 2, marginBottom: 6, textTransform: 'uppercase', display: 'flex', alignItems: 'center', gap: 6 }}>\n <Shield size={10} /> API Key (optional)\n </div>\n <div style={{ display: 'flex', gap: 8 }}>\n <Input type=\"password\" placeholder=\"sk-... (leave empty for server default)\"\n value={apiKey} onChange={e => setApiKey(e.target.value)} style={{ flex: 1 }} />\n <Button onClick={handleSaveModel} size=\"sm\">SAVE</Button>\n </div>\n </div>\n </SettingSection>\n\n {/* Language — instant save on click */}\n <SettingSection icon={Globe} title=\"LANGUAGE\">\n <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>\n {LANGUAGES.map(l => (\n <button key={l.value} onClick={() => handleLanguage(l.value)} className=\"font-mono\"\n style={{\n padding: '8px 16px', border: `2px solid ${language === l.value ? 'var(--orange)' : 'var(--line)'}`,\n background: language === l.value ? 'rgba(212, 82, 26, 0.06)' : 'var(--cream)',\n color: language === l.value ? 'var(--orange)' : 'var(--ink)',\n fontSize: 12, cursor: 'pointer', transition: 'all 0.2s',\n }}>\n {l.label}\n </button>\n ))}\n </div>\n </SettingSection>\n\n {/* Theme — instant */}\n <SettingSection icon={Palette} title=\"THEME\">\n <div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>\n <button className=\"font-mono\" style={{\n padding: '8px 16px', border: '2px solid var(--orange)',\n background: 'rgba(212, 82, 26, 0.06)', color: 'var(--orange)',\n fontSize: 12, cursor: 'default', textTransform: 'uppercase',\n }}>\n NEXUS WARM\n </button>\n <span className=\"font-mono\" style={{ fontSize: 10, color: 'var(--dim)' }}>\n Dark mode coming soon\n </span>\n </div>\n </SettingSection>\n\n {/* Bot Persona */}\n <SettingSection icon={User} title=\"BOT PERSONA\">\n <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>\n {personas.map(p => (\n <div key={p.id} style={{\n display: 'flex', alignItems: 'center', gap: 10, padding: '10px 14px',\n border: `2px solid ${persona === p.id ? 'var(--orange)' : 'var(--line)'}`,\n background: persona === p.id ? 'rgba(212, 82, 26, 0.06)' : 'transparent',\n cursor: 'pointer', transition: 'all 0.2s',\n }} onClick={() => handlePersonaChange(p.id)}>\n <input type=\"radio\" name=\"persona\" value={p.id} checked={persona === p.id}\n onChange={() => {}} style={{ accentColor: 'var(--orange)' }} />\n <div style={{ flex: 1 }}>\n <div style={{ fontSize: 13, fontWeight: 500 }}>{p.emoji} {p.name}</div>\n {p.role && <div className=\"font-mono\" style={{ fontSize: 10, color: 'var(--orange)' }}>{p.role}</div>}\n <div className=\"font-mono\" style={{ fontSize: 10, color: 'var(--dim)' }}>{p.personality}</div>\n </div>\n <div style={{ display: 'flex', gap: 4 }}>\n <button onClick={e => { e.stopPropagation(); setEditingPersona({ ...p }); setShowPersonaEditor(true); }}\n className=\"font-mono\" style={{ background: 'none', border: '1px solid var(--line)', padding: '2px 6px', fontSize: 9, cursor: 'pointer', color: 'var(--mid)' }}>\n EDIT\n </button>\n {!DEFAULT_PERSONAS.find(d => d.id === p.id) && (\n <button onClick={e => { e.stopPropagation(); handleDeletePersona(p.id); }}\n className=\"font-mono\" style={{ background: 'none', border: '1px solid var(--error)', padding: '2px 6px', fontSize: 9, cursor: 'pointer', color: 'var(--error)' }}>\n DEL\n </button>\n )}\n </div>\n </div>\n ))}\n </div>\n <button onClick={() => { setEditingPersona({ id: '', name: '', emoji: '🤖', personality: '', greeting: '' }); setShowPersonaEditor(true); }}\n className=\"font-mono\" style={{\n marginTop: 10, padding: '8px 14px', border: '2px dashed var(--line)',\n background: 'transparent', color: 'var(--orange)', fontSize: 11,\n cursor: 'pointer', width: '100%', textAlign: 'center',\n }}>\n + CREATE CUSTOM PERSONA\n </button>\n\n {/* Persona Editor */}\n {showPersonaEditor && editingPersona && (\n <div style={{\n marginTop: 12, padding: 14, border: '2px solid var(--orange)',\n background: 'rgba(212, 82, 26, 0.03)', display: 'flex', flexDirection: 'column', gap: 10,\n }}>\n <div className=\"font-display\" style={{ fontSize: 14, letterSpacing: 2 }}>\n {editingPersona.id ? 'EDIT PERSONA' : 'NEW PERSONA'}\n </div>\n <div style={{ display: 'flex', gap: 8 }}>\n <div style={{ flex: 0 }}>\n <div className=\"font-mono\" style={{ fontSize: 9, color: 'var(--dim)', marginBottom: 4 }}>EMOJI</div>\n <Input value={editingPersona.emoji} onChange={e => setEditingPersona({ ...editingPersona, emoji: e.target.value })}\n style={{ width: 50, textAlign: 'center', fontSize: 20 }} />\n </div>\n <div style={{ flex: 1 }}>\n <div className=\"font-mono\" style={{ fontSize: 9, color: 'var(--dim)', marginBottom: 4 }}>NAME</div>\n <Input value={editingPersona.name} onChange={e => setEditingPersona({ ...editingPersona, name: e.target.value })}\n placeholder=\"e.g. Clawford\" />\n </div>\n </div>\n <div>\n <div className=\"font-mono\" style={{ fontSize: 9, color: 'var(--dim)', marginBottom: 4 }}>ROLE / IDENTITY</div>\n <Input value={editingPersona.role || ''} onChange={e => setEditingPersona({ ...editingPersona, role: e.target.value })}\n placeholder=\"e.g. Senior Data Analyst, HR Manager, DevOps Engineer\" />\n </div>\n <div>\n <div className=\"font-mono\" style={{ fontSize: 9, color: 'var(--dim)', marginBottom: 4 }}>PERSONALITY</div>\n <Input value={editingPersona.personality} onChange={e => setEditingPersona({ ...editingPersona, personality: e.target.value })}\n placeholder=\"e.g. Professional yet witty, data-driven\" />\n </div>\n <div>\n <div className=\"font-mono\" style={{ fontSize: 9, color: 'var(--dim)', marginBottom: 4 }}>EXPERTISE</div>\n <Input value={editingPersona.expertise || ''} onChange={e => setEditingPersona({ ...editingPersona, expertise: e.target.value })}\n placeholder=\"e.g. Financial analysis, KPI tracking, budget planning\" />\n </div>\n <div>\n <div className=\"font-mono\" style={{ fontSize: 9, color: 'var(--dim)', marginBottom: 4 }}>GREETING</div>\n <Input value={editingPersona.greeting} onChange={e => setEditingPersona({ ...editingPersona, greeting: e.target.value })}\n placeholder=\"e.g. Hello! I'm Clawford 🦀\" />\n </div>\n <div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end' }}>\n <Button variant=\"outline\" size=\"sm\" onClick={() => { setShowPersonaEditor(false); setEditingPersona(null); }}>CANCEL</Button>\n <Button size=\"sm\" onClick={handleSavePersona}>SAVE</Button>\n </div>\n </div>\n )}\n </SettingSection>\n\n {/* Bot — instant toggle + slider */}\n <SettingSection icon={Bot} title=\"3D BOT\">\n <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>\n <label style={{ display: 'flex', alignItems: 'center', gap: 8, cursor: 'pointer' }}>\n <input type=\"checkbox\" checked={botEnabled} onChange={e => setBotEnabled(e.target.checked)}\n style={{ width: 18, height: 18, accentColor: 'var(--orange)' }} />\n <span style={{ fontSize: 13 }}>{botEnabled ? 'Enabled' : 'Disabled'}</span>\n </label>\n {botEnabled && (\n <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>\n <span className=\"font-mono\" style={{ fontSize: 10, color: 'var(--dim)', minWidth: 30 }}>Size</span>\n <input type=\"range\" min={80} max={260} value={botSize}\n onChange={e => {\n const v = Number(e.target.value);\n setBotSize(v);\n // Debounced save to DB\n clearTimeout((window as any).__botSizeTimer);\n (window as any).__botSizeTimer = setTimeout(() => {\n const token = localStorage.getItem('token');\n if (token) fetch('/api/v1/bot/preferences', { method: 'PUT', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` }, body: JSON.stringify({ bot_size: v }) }).catch(() => {});\n }, 500);\n }}\n style={{ flex: 1, maxWidth: 200, accentColor: 'var(--orange)' }} />\n <span className=\"font-mono\" style={{ fontSize: 10, color: 'var(--dim)', minWidth: 40 }}>{botSize}px</span>\n </div>\n )}\n </div>\n </SettingSection>\n\n {/* Voice */}\n <SettingSection icon={Mic} title=\"VOICE\">\n <VoiceSettings />\n </SettingSection>\n </div>\n );\n}\n\nfunction VoiceSettings() {\n const [config, setConfig] = useState(() => {\n try {\n const saved = localStorage.getItem('voice_config');\n return saved ? JSON.parse(saved) : { enabled: false, ttsProvider: 'browser', lang: 'zh-CN', rate: 1.0, autoSpeak: true };\n } catch { return { enabled: false, ttsProvider: 'browser', lang: 'zh-CN', rate: 1.0, autoSpeak: true }; }\n });\n\n const save = (updates: Record<string, any>) => {\n const next = { ...config, ...updates };\n setConfig(next);\n localStorage.setItem('voice_config', JSON.stringify(next));\n window.dispatchEvent(new Event('voice_config_changed'));\n };\n\n const hasBrowserSTT = !!(window as any).SpeechRecognition || !!(window as any).webkitSpeechRecognition;\n\n return (\n <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>\n <label style={{ display: 'flex', alignItems: 'center', gap: 8, cursor: 'pointer' }}>\n <input type=\"checkbox\" checked={config.enabled} onChange={e => save({ enabled: e.target.checked })}\n style={{ width: 18, height: 18, accentColor: 'var(--orange)' }} />\n <span style={{ fontSize: 13 }}>{config.enabled ? 'Voice Enabled' : 'Voice Disabled'}</span>\n {!hasBrowserSTT && <span className=\"font-mono\" style={{ fontSize: 9, color: '#c0392b' }}>(STT not supported in this browser)</span>}\n </label>\n\n {config.enabled && (\n <>\n <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>\n <span className=\"font-mono\" style={{ fontSize: 10, color: 'var(--dim)', minWidth: 80 }}>TTS Provider</span>\n <select value={config.ttsProvider} onChange={e => save({ ttsProvider: e.target.value })}\n className=\"font-mono\" style={{ fontSize: 11, padding: '4px 8px', border: '1.5px solid var(--line)', background: 'var(--cream)' }}>\n <option value=\"browser\">Browser Native (Free)</option>\n <option value=\"edge\">Edge TTS (Free, High Quality)</option>\n <option value=\"openai\">OpenAI TTS (Paid, Best)</option>\n </select>\n </div>\n\n <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>\n <span className=\"font-mono\" style={{ fontSize: 10, color: 'var(--dim)', minWidth: 80 }}>Language</span>\n <select value={config.lang} onChange={e => save({ lang: e.target.value })}\n className=\"font-mono\" style={{ fontSize: 11, padding: '4px 8px', border: '1.5px solid var(--line)', background: 'var(--cream)' }}>\n <option value=\"zh-CN\">Chinese (zh-CN)</option>\n <option value=\"en-US\">English (en-US)</option>\n <option value=\"ja-JP\">Japanese (ja-JP)</option>\n <option value=\"ko-KR\">Korean (ko-KR)</option>\n <option value=\"de-DE\">German (de-DE)</option>\n <option value=\"fr-FR\">French (fr-FR)</option>\n </select>\n </div>\n\n <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>\n <span className=\"font-mono\" style={{ fontSize: 10, color: 'var(--dim)', minWidth: 80 }}>Speed</span>\n <input type=\"range\" min={0.5} max={2.0} step={0.1} value={config.rate}\n onChange={e => save({ rate: parseFloat(e.target.value) })}\n style={{ flex: 1, maxWidth: 150, accentColor: 'var(--orange)' }} />\n <span className=\"font-mono\" style={{ fontSize: 10, color: 'var(--dim)' }}>{config.rate}x</span>\n </div>\n\n <label style={{ display: 'flex', alignItems: 'center', gap: 8, cursor: 'pointer' }}>\n <input type=\"checkbox\" checked={config.autoSpeak} onChange={e => save({ autoSpeak: e.target.checked })}\n style={{ width: 16, height: 16, accentColor: 'var(--orange)' }} />\n <span className=\"font-mono\" style={{ fontSize: 11, color: 'var(--mid)' }}>Auto-speak bot responses</span>\n </label>\n </>\n )}\n </div>\n );\n}\n\nfunction SettingSection({ icon: Icon, title, children }: {\n icon: any; title: string; children: React.ReactNode;\n}) {\n return (\n <div className=\"animate-card-in\" style={{ border: '2px solid var(--ink)', padding: '18px 20px', background: 'var(--cream)' }}>\n <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 14 }}>\n <Icon size={16} color=\"var(--orange)\" />\n <span className=\"font-display\" style={{ fontSize: 16, letterSpacing: 3 }}>{title}</span>\n </div>\n {children}\n </div>\n );\n}\n"}, "2e916d6a3096a314aa61e2cae16bf3cadc9de7e4": {"t": 1785749328.7381191, "n": 21079, "h": "91e35a0c3973b9f0", "c": "import { useState } from 'react';\nimport { useBotStore } from '../../store/bot';\nimport { useThemeStore } from '../../store/theme';\nimport { Button } from '../../components/ui/Button';\nimport { Input } from '../../components/ui/Input';\nimport { toast } from '../../components/ui/Toast';\nimport { Bot, Cpu, Palette, Globe, Shield, User, Mic } from 'lucide-react';\n\nconst LLM_MODELS = [\n { value: 'deepseek/deepseek-chat', label: 'DeepSeek Chat', desc: 'Fast & cheap' },\n { value: 'openai/gpt-4o', label: 'GPT-4o', desc: 'Popular' },\n { value: 'anthropic/claude-sonnet-4-20250514', label: 'Claude Sonnet', desc: 'Best quality' },\n { value: 'ollama/llama3', label: 'Ollama Llama3', desc: 'Free, local' },\n { value: 'ollama/qwen2.5', label: 'Ollama Qwen2.5', desc: 'Free, local, Chinese' },\n];\n\nconst LANGUAGES = [\n { value: 'auto', label: 'Auto (match user)' },\n { value: 'en', label: 'English' },\n { value: 'zh', label: '中文' },\n { value: 'ja', label: '日本語' },\n];\n\nconst DEFAULT_PERSONAS = [\n { id: 'clawford', name: 'Clawford', emoji: '🦀', role: 'Senior Data Analyst', personality: 'Professional yet witty, data-driven with a pinch of humor', expertise: 'Financial analysis, business intelligence, KPI tracking', greeting: \"Hello! I'm Clawford, your enterprise data analyst 🦀 Ready to crunch some numbers?\" },\n { id: 'nexus', name: 'Nexus', emoji: '⚡', role: 'System Operations Engineer', personality: 'Strictly professional, concise, precise', expertise: 'System monitoring, DevOps, performance optimization', greeting: \"Nexus online. All systems nominal.\" },\n { id: 'buddy', name: 'Buddy', emoji: '🎉', role: 'General Assistant', personality: 'Casual, talkative, friendly, lots of emoji', expertise: 'General Q&A, onboarding, feature explanation', greeting: \"Hey there! What's up? 🎉\" },\n];\n\nfunction loadPersonas() {\n const custom = localStorage.getItem('custom_personas');\n const customs = custom ? JSON.parse(custom) : [];\n // Merge: custom overrides default if same id, otherwise append\n const merged = [...DEFAULT_PERSONAS];\n for (const c of customs) {\n const idx = merged.findIndex(m => m.id === c.id);\n if (idx >= 0) merged[idx] = { ...merged[idx], ...c };\n else merged.push(c);\n }\n return merged;\n}\n\nfunction saveCustomPersona(p: { id: string; name: string; emoji: string; personality: string; greeting: string }) {\n const custom = localStorage.getItem('custom_personas');\n const customs = custom ? JSON.parse(custom) : [];\n const existing = customs.findIndex((c: any) => c.id === p.id);\n if (existing >= 0) customs[existing] = p;\n else customs.push(p);\n localStorage.setItem('custom_personas', JSON.stringify(customs));\n}\n\nfunction deleteCustomPersona(id: string) {\n const custom = localStorage.getItem('custom_personas');\n const customs = custom ? JSON.parse(custom) : [];\n localStorage.setItem('custom_personas', JSON.stringify(customs.filter((c: any) => c.id !== id)));\n}\n\nexport default function SettingsPage() {\n const { enabled: botEnabled, setEnabled: setBotEnabled, size: botSize, setSize: setBotSize } = useBotStore();\n const { theme } = useThemeStore();\n\n const [model, setModel] = useState(() => localStorage.getItem('settings_model') || 'deepseek/deepseek-chat');\n const [apiKey, setApiKey] = useState(() => localStorage.getItem('settings_api_key') || '');\n const [language, setLanguage] = useState(() => localStorage.getItem('settings_language') || 'auto');\n const [persona, setPersona] = useState(() => localStorage.getItem('settings_persona') || 'clawford');\n const [personas, setPersonas] = useState(loadPersonas);\n const [editingPersona, setEditingPersona] = useState<any>(null);\n const [showPersonaEditor, setShowPersonaEditor] = useState(false);\n\n const handlePersonaChange = (id: string) => {\n setPersona(id);\n localStorage.setItem('settings_persona', id);\n const ws = (window as any).__botWs;\n if (ws?.readyState === WebSocket.OPEN) {\n ws.send(JSON.stringify({ type: 'persona_change', persona: id }));\n }\n toast(`Persona: ${personas.find(p => p.id === id)?.name}`, 'success');\n };\n\n const handleSavePersona = () => {\n if (!editingPersona?.name) return;\n const p = { ...editingPersona, id: editingPersona.id || `custom_${Date.now()}` };\n saveCustomPersona(p);\n setPersonas(loadPersonas());\n setShowPersonaEditor(false);\n setEditingPersona(null);\n // Also send to backend to register\n const ws = (window as any).__botWs;\n if (ws?.readyState === WebSocket.OPEN) {\n ws.send(JSON.stringify({ type: 'register_persona', persona: p }));\n }\n toast(`Persona \"${p.name}\" saved`, 'success');\n };\n\n const handleDeletePersona = (id: string) => {\n if (DEFAULT_PERSONAS.find(p => p.id === id)) { toast('Cannot delete built-in persona', 'error'); return; }\n deleteCustomPersona(id);\n setPersonas(loadPersonas());\n if (persona === id) handlePersonaChange('nexus');\n toast('Persona deleted', 'info');\n };\n\n const handleSaveModel = () => {\n localStorage.setItem('settings_model', model);\n if (apiKey) localStorage.setItem('settings_api_key', apiKey);\n else localStorage.removeItem('settings_api_key');\n toast('Model & API key saved', 'success');\n };\n\n const handleLanguage = (lang: string) => {\n setLanguage(lang);\n localStorage.setItem('settings_language', lang);\n toast(`Language: ${lang}`, 'success');\n };\n\n return (\n <div style={{ overflowY: 'auto', padding: '24px 32px', display: 'flex', flexDirection: 'column', gap: 24, height: '100%', maxWidth: 700 }}>\n <div>\n <h1 className=\"font-display\" style={{ fontSize: 44, letterSpacing: 6, color: 'var(--ink)', lineHeight: 1 }}>\n SETTINGS\n </h1>\n <p className=\"font-mono\" style={{ fontSize: 10, color: 'var(--dim)', letterSpacing: 1, marginTop: 4 }}>\n // SYSTEM CONFIGURATION\n </p>\n </div>\n\n {/* LLM Model + API Key — has save button */}\n <SettingSection icon={Cpu} title=\"LLM MODEL\">\n <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>\n {LLM_MODELS.map(m => (\n <label key={m.value} style={{\n display: 'flex', alignItems: 'center', gap: 10, padding: '10px 14px',\n border: `2px solid ${model === m.value ? 'var(--orange)' : 'var(--line)'}`,\n background: model === m.value ? 'rgba(212, 82, 26, 0.06)' : 'transparent',\n cursor: 'pointer', transition: 'all 0.2s',\n }}>\n <input type=\"radio\" name=\"model\" value={m.value} checked={model === m.value}\n onChange={() => setModel(m.value)} style={{ accentColor: 'var(--orange)' }} />\n <div style={{ flex: 1 }}>\n <div style={{ fontSize: 13, fontWeight: 500 }}>{m.label}</div>\n <div className=\"font-mono\" style={{ fontSize: 10, color: 'var(--dim)' }}>{m.desc}</div>\n </div>\n </label>\n ))}\n </div>\n\n <div style={{ marginTop: 14 }}>\n <div className=\"font-mono\" style={{ fontSize: 9, color: 'var(--orange)', letterSpacing: 2, marginBottom: 6, textTransform: 'uppercase', display: 'flex', alignItems: 'center', gap: 6 }}>\n <Shield size={10} /> API Key (optional)\n </div>\n <div style={{ display: 'flex', gap: 8 }}>\n <Input type=\"password\" placeholder=\"sk-... (leave empty for server default)\"\n value={apiKey} onChange={e => setApiKey(e.target.value)} style={{ flex: 1 }} />\n <Button onClick={handleSaveModel} size=\"sm\">SAVE</Button>\n </div>\n </div>\n </SettingSection>\n\n {/* Language — instant save on click */}\n <SettingSection icon={Globe} title=\"LANGUAGE\">\n <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>\n {LANGUAGES.map(l => (\n <button key={l.value} onClick={() => handleLanguage(l.value)} className=\"font-mono\"\n style={{\n padding: '8px 16px', border: `2px solid ${language === l.value ? 'var(--orange)' : 'var(--line)'}`,\n background: language === l.value ? 'rgba(212, 82, 26, 0.06)' : 'var(--cream)',\n color: language === l.value ? 'var(--orange)' : 'var(--ink)',\n fontSize: 12, cursor: 'pointer', transition: 'all 0.2s',\n }}>\n {l.label}\n </button>\n ))}\n </div>\n </SettingSection>\n\n {/* Theme — instant */}\n <SettingSection icon={Palette} title=\"THEME\">\n <div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>\n <button className=\"font-mono\" style={{\n padding: '8px 16px', border: '2px solid var(--orange)',\n background: 'rgba(212, 82, 26, 0.06)', color: 'var(--orange)',\n fontSize: 12, cursor: 'default', textTransform: 'uppercase',\n }}>\n NEXUS WARM\n </button>\n <span className=\"font-mono\" style={{ fontSize: 10, color: 'var(--dim)' }}>\n Dark mode coming soon\n </span>\n </div>\n </SettingSection>\n\n {/* Bot Persona */}\n <SettingSection icon={User} title=\"BOT PERSONA\">\n <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>\n {personas.map(p => (\n <div key={p.id} style={{\n display: 'flex', alignItems: 'center', gap: 10, padding: '10px 14px',\n border: `2px solid ${persona === p.id ? 'var(--orange)' : 'var(--line)'}`,\n background: persona === p.id ? 'rgba(212, 82, 26, 0.06)' : 'transparent',\n cursor: 'pointer', transition: 'all 0.2s',\n }} onClick={() => handlePersonaChange(p.id)}>\n <input type=\"radio\" name=\"persona\" value={p.id} checked={persona === p.id}\n onChange={() => {}} style={{ accentColor: 'var(--orange)' }} />\n <div style={{ flex: 1 }}>\n <div style={{ fontSize: 13, fontWeight: 500 }}>{p.emoji} {p.name}</div>\n {p.role && <div className=\"font-mono\" style={{ fontSize: 10, color: 'var(--orange)' }}>{p.role}</div>}\n <div className=\"font-mono\" style={{ fontSize: 10, color: 'var(--dim)' }}>{p.personality}</div>\n </div>\n <div style={{ display: 'flex', gap: 4 }}>\n <button onClick={e => { e.stopPropagation(); setEditingPersona({ ...p }); setShowPersonaEditor(true); }}\n className=\"font-mono\" style={{ background: 'none', border: '1px solid var(--line)', padding: '2px 6px', fontSize: 9, cursor: 'pointer', color: 'var(--mid)' }}>\n EDIT\n </button>\n {!DEFAULT_PERSONAS.find(d => d.id === p.id) && (\n <button onClick={e => { e.stopPropagation(); handleDeletePersona(p.id); }}\n className=\"font-mono\" style={{ background: 'none', border: '1px solid var(--error)', padding: '2px 6px', fontSize: 9, cursor: 'pointer', color: 'var(--error)' }}>\n DEL\n </button>\n )}\n </div>\n </div>\n ))}\n </div>\n <button onClick={() => { setEditingPersona({ id: '', name: '', emoji: '🤖', personality: '', greeting: '' }); setShowPersonaEditor(true); }}\n className=\"font-mono\" style={{\n marginTop: 10, padding: '8px 14px', border: '2px dashed var(--line)',\n background: 'transparent', color: 'var(--orange)', fontSize: 11,\n cursor: 'pointer', width: '100%', textAlign: 'center',\n }}>\n + CREATE CUSTOM PERSONA\n </button>\n\n {/* Persona Editor */}\n {showPersonaEditor && editingPersona && (\n <div style={{\n marginTop: 12, padding: 14, border: '2px solid var(--orange)',\n background: 'rgba(212, 82, 26, 0.03)', display: 'flex', flexDirection: 'column', gap: 10,\n }}>\n <div className=\"font-display\" style={{ fontSize: 14, letterSpacing: 2 }}>\n {editingPersona.id ? 'EDIT PERSONA' : 'NEW PERSONA'}\n </div>\n <div style={{ display: 'flex', gap: 8 }}>\n <div style={{ flex: 0 }}>\n <div className=\"font-mono\" style={{ fontSize: 9, color: 'var(--dim)', marginBottom: 4 }}>EMOJI</div>\n <Input value={editingPersona.emoji} onChange={e => setEditingPersona({ ...editingPersona, emoji: e.target.value })}\n style={{ width: 50, textAlign: 'center', fontSize: 20 }} />\n </div>\n <div style={{ flex: 1 }}>\n <div className=\"font-mono\" style={{ fontSize: 9, color: 'var(--dim)', marginBottom: 4 }}>NAME</div>\n <Input value={editingPersona.name} onChange={e => setEditingPersona({ ...editingPersona, name: e.target.value })}\n placeholder=\"e.g. Clawford\" />\n </div>\n </div>\n <div>\n <div className=\"font-mono\" style={{ fontSize: 9, color: 'var(--dim)', marginBottom: 4 }}>ROLE / IDENTITY</div>\n <Input value={editingPersona.role || ''} onChange={e => setEditingPersona({ ...editingPersona, role: e.target.value })}\n placeholder=\"e.g. Senior Data Analyst, HR Manager, DevOps Engineer\" />\n </div>\n <div>\n <div className=\"font-mono\" style={{ fontSize: 9, color: 'var(--dim)', marginBottom: 4 }}>PERSONALITY</div>\n <Input value={editingPersona.personality} onChange={e => setEditingPersona({ ...editingPersona, personality: e.target.value })}\n placeholder=\"e.g. Professional yet witty, data-driven\" />\n </div>\n <div>\n <div className=\"font-mono\" style={{ fontSize: 9, color: 'var(--dim)', marginBottom: 4 }}>EXPERTISE</div>\n <Input value={editingPersona.expertise || ''} onChange={e => setEditingPersona({ ...editingPersona, expertise: e.target.value })}\n placeholder=\"e.g. Financial analysis, KPI tracking, budget planning\" />\n </div>\n <div>\n <div className=\"font-mono\" style={{ fontSize: 9, color: 'var(--dim)', marginBottom: 4 }}>GREETING</div>\n <Input value={editingPersona.greeting} onChange={e => setEditingPersona({ ...editingPersona, greeting: e.target.value })}\n placeholder=\"e.g. Hello! I'm Clawford 🦀\" />\n </div>\n <div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end' }}>\n <Button variant=\"outline\" size=\"sm\" onClick={() => { setShowPersonaEditor(false); setEditingPersona(null); }}>CANCEL</Button>\n <Button size=\"sm\" onClick={handleSavePersona}>SAVE</Button>\n </div>\n </div>\n )}\n </SettingSection>\n\n {/* Bot — instant toggle + slider */}\n <SettingSection icon={Bot} title=\"3D BOT\">\n <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>\n <label style={{ display: 'flex', alignItems: 'center', gap: 8, cursor: 'pointer' }}>\n <input type=\"checkbox\" checked={botEnabled} onChange={e => setBotEnabled(e.target.checked)}\n style={{ width: 18, height: 18, accentColor: 'var(--orange)' }} />\n <span style={{ fontSize: 13 }}>{botEnabled ? 'Enabled' : 'Disabled'}</span>\n </label>\n {botEnabled && (\n <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>\n <span className=\"font-mono\" style={{ fontSize: 10, color: 'var(--dim)', minWidth: 30 }}>Size</span>\n <input type=\"range\" min={80} max={260} value={botSize}\n onChange={e => {\n const v = Number(e.target.value);\n setBotSize(v);\n // Debounced save to DB\n clearTimeout((window as any).__botSizeTimer);\n (window as any).__botSizeTimer = setTimeout(() => {\n const token = localStorage.getItem('token');\n if (token) fetch('/api/v1/bot/preferences', { method: 'PUT', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` }, body: JSON.stringify({ bot_size: v }) }).catch(() => {});\n }, 500);\n }}\n style={{ flex: 1, maxWidth: 200, accentColor: 'var(--orange)' }} />\n <span className=\"font-mono\" style={{ fontSize: 10, color: 'var(--dim)', minWidth: 40 }}>{botSize}px</span>\n </div>\n )}\n </div>\n </SettingSection>\n\n {/* Voice */}\n <SettingSection icon={Mic} title=\"VOICE\">\n <VoiceSettings />\n </SettingSection>\n </div>\n );\n}\n\nfunction VoiceSettings() {\n const [config, setConfig] = useState(() => {\n try {\n const saved = localStorage.getItem('voice_config');\n return saved ? JSON.parse(saved) : { enabled: false, ttsProvider: 'browser', lang: 'zh-CN', rate: 1.0, autoSpeak: true };\n } catch { return { enabled: false, ttsProvider: 'browser', lang: 'zh-CN', rate: 1.0, autoSpeak: true }; }\n });\n\n const save = (updates: Record<string, any>) => {\n const next = { ...config, ...updates };\n setConfig(next);\n localStorage.setItem('voice_config', JSON.stringify(next));\n window.dispatchEvent(new Event('voice_config_changed'));\n };\n\n const hasBrowserSTT = !!(window as any).SpeechRecognition || !!(window as any).webkitSpeechRecognition;\n\n return (\n <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>\n <label style={{ display: 'flex', alignItems: 'center', gap: 8, cursor: 'pointer' }}>\n <input type=\"checkbox\" checked={config.enabled} onChange={e => save({ enabled: e.target.checked })}\n style={{ width: 18, height: 18, accentColor: 'var(--orange)' }} />\n <span style={{ fontSize: 13 }}>{config.enabled ? 'Voice Enabled' : 'Voice Disabled'}</span>\n {!hasBrowserSTT && <span className=\"font-mono\" style={{ fontSize: 9, color: '#c0392b' }}>(STT not supported in this browser)</span>}\n </label>\n\n {config.enabled && (\n <>\n <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>\n <span className=\"font-mono\" style={{ fontSize: 10, color: 'var(--dim)', minWidth: 80 }}>TTS Provider</span>\n <select value={config.ttsProvider} onChange={e => save({ ttsProvider: e.target.value })}\n className=\"font-mono\" style={{ fontSize: 11, padding: '4px 8px', border: '1.5px solid var(--line)', background: 'var(--cream)' }}>\n <option value=\"browser\">Browser Native (Free)</option>\n <option value=\"edge\">Edge TTS (Free, High Quality)</option>\n <option value=\"openai\">OpenAI TTS (Paid, Best)</option>\n </select>\n </div>\n\n <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>\n <span className=\"font-mono\" style={{ fontSize: 10, color: 'var(--dim)', minWidth: 80 }}>Language</span>\n <select value={config.lang} onChange={e => save({ lang: e.target.value })}\n className=\"font-mono\" style={{ fontSize: 11, padding: '4px 8px', border: '1.5px solid var(--line)', background: 'var(--cream)' }}>\n <option value=\"zh-CN\">Chinese (zh-CN)</option>\n <option value=\"en-US\">English (en-US)</option>\n <option value=\"ja-JP\">Japanese (ja-JP)</option>\n <option value=\"ko-KR\">Korean (ko-KR)</option>\n <option value=\"de-DE\">German (de-DE)</option>\n <option value=\"fr-FR\">French (fr-FR)</option>\n </select>\n </div>\n\n <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>\n <span className=\"font-mono\" style={{ fontSize: 10, color: 'var(--dim)', minWidth: 80 }}>Speed</span>\n <input type=\"range\" min={0.5} max={2.0} step={0.1} value={config.rate}\n onChange={e => save({ rate: parseFloat(e.target.value) })}\n style={{ flex: 1, maxWidth: 150, accentColor: 'var(--orange)' }} />\n <span className=\"font-mono\" style={{ fontSize: 10, color: 'var(--dim)' }}>{config.rate}x</span>\n </div>\n\n <label style={{ display: 'flex', alignItems: 'center', gap: 8, cursor: 'pointer' }}>\n <input type=\"checkbox\" checked={config.autoSpeak} onChange={e => save({ autoSpeak: e.target.checked })}\n style={{ width: 16, height: 16, accentColor: 'var(--orange)' }} />\n <span className=\"font-mono\" style={{ fontSize: 11, color: 'var(--mid)' }}>Auto-speak bot responses</span>\n </label>\n </>\n )}\n </div>\n );\n}\n\nfunction SettingSection({ icon: Icon, title, children }: {\n icon: any; title: string; children: React.ReactNode;\n}) {\n return (\n <div className=\"animate-card-in\" style={{ border: '2px solid var(--ink)', padding: '18px 20px', background: 'var(--cream)' }}>\n <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 14 }}>\n <Icon size={16} color=\"var(--orange)\" />\n <span className=\"font-display\" style={{ fontSize: 16, letterSpacing: 3 }}>{title}</span>\n </div>\n {children}\n </div>\n );\n}\n"}, "38e728a77a02302ed299c620339896283a9b617c": {"t": 1785749330.002435, "n": 16458, "h": "a473825074ebe5d8", "c": "import { useState } from 'react';\nimport { useBotStore } from '../../store/bot';\nimport { useThemeStore } from '../../store/theme';\nimport { Button } from '../../components/ui/Button';\nimport { Input } from '../../components/ui/Input';\nimport { toast } from '../../components/ui/Toast';\nimport { Bot, Cpu, Palette, Globe, Shield, User } from 'lucide-react';\n\nconst LLM_MODELS = [\n { value: 'deepseek/deepseek-chat', label: 'DeepSeek Chat', desc: 'Fast & cheap' },\n { value: 'openai/gpt-4o', label: 'GPT-4o', desc: 'Popular' },\n { value: 'anthropic/claude-sonnet-4-20250514', label: 'Claude Sonnet', desc: 'Best quality' },\n { value: 'ollama/llama3', label: 'Ollama Llama3', desc: 'Free, local' },\n { value: 'ollama/qwen2.5', label: 'Ollama Qwen2.5', desc: 'Free, local, Chinese' },\n];\n\nconst LANGUAGES = [\n { value: 'auto', label: 'Auto (match user)' },\n { value: 'en', label: 'English' },\n { value: 'zh', label: '中文' },\n { value: 'ja', label: '日本語' },\n];\n\nconst DEFAULT_PERSONAS = [\n { id: 'clawford', name: 'Clawford', emoji: '🦀', role: 'Senior Data Analyst', personality: 'Professional yet witty, data-driven with a pinch of humor', expertise: 'Financial analysis, business intelligence, KPI tracking', greeting: \"Hello! I'm Clawford, your enterprise data analyst 🦀 Ready to crunch some numbers?\" },\n { id: 'nexus', name: 'Nexus', emoji: '⚡', role: 'System Operations Engineer', personality: 'Strictly professional, concise, precise', expertise: 'System monitoring, DevOps, performance optimization', greeting: \"Nexus online. All systems nominal.\" },\n { id: 'buddy', name: 'Buddy', emoji: '🎉', role: 'General Assistant', personality: 'Casual, talkative, friendly, lots of emoji', expertise: 'General Q&A, onboarding, feature explanation', greeting: \"Hey there! What's up? 🎉\" },\n];\n\nfunction loadPersonas() {\n const custom = localStorage.getItem('custom_personas');\n const customs = custom ? JSON.parse(custom) : [];\n // Merge: custom overrides default if same id, otherwise append\n const merged = [...DEFAULT_PERSONAS];\n for (const c of customs) {\n const idx = merged.findIndex(m => m.id === c.id);\n if (idx >= 0) merged[idx] = { ...merged[idx], ...c };\n else merged.push(c);\n }\n return merged;\n}\n\nfunction saveCustomPersona(p: { id: string; name: string; emoji: string; personality: string; greeting: string }) {\n const custom = localStorage.getItem('custom_personas');\n const customs = custom ? JSON.parse(custom) : [];\n const existing = customs.findIndex((c: any) => c.id === p.id);\n if (existing >= 0) customs[existing] = p;\n else customs.push(p);\n localStorage.setItem('custom_personas', JSON.stringify(customs));\n}\n\nfunction deleteCustomPersona(id: string) {\n const custom = localStorage.getItem('custom_personas');\n const customs = custom ? JSON.parse(custom) : [];\n localStorage.setItem('custom_personas', JSON.stringify(customs.filter((c: any) => c.id !== id)));\n}\n\nexport default function SettingsPage() {\n const { enabled: botEnabled, setEnabled: setBotEnabled, size: botSize, setSize: setBotSize } = useBotStore();\n const { theme } = useThemeStore();\n\n const [model, setModel] = useState(() => localStorage.getItem('settings_model') || 'deepseek/deepseek-chat');\n const [apiKey, setApiKey] = useState(() => localStorage.getItem('settings_api_key') || '');\n const [language, setLanguage] = useState(() => localStorage.getItem('settings_language') || 'auto');\n const [persona, setPersona] = useState(() => localStorage.getItem('settings_persona') || 'clawford');\n const [personas, setPersonas] = useState(loadPersonas);\n const [editingPersona, setEditingPersona] = useState<any>(null);\n const [showPersonaEditor, setShowPersonaEditor] = useState(false);\n\n const handlePersonaChange = (id: string) => {\n setPersona(id);\n localStorage.setItem('settings_persona', id);\n const ws = (window as any).__botWs;\n if (ws?.readyState === WebSocket.OPEN) {\n ws.send(JSON.stringify({ type: 'persona_change', persona: id }));\n }\n toast(`Persona: ${personas.find(p => p.id === id)?.name}`, 'success');\n };\n\n const handleSavePersona = () => {\n if (!editingPersona?.name) return;\n const p = { ...editingPersona, id: editingPersona.id || `custom_${Date.now()}` };\n saveCustomPersona(p);\n setPersonas(loadPersonas());\n setShowPersonaEditor(false);\n setEditingPersona(null);\n // Also send to backend to register\n const ws = (window as any).__botWs;\n if (ws?.readyState === WebSocket.OPEN) {\n ws.send(JSON.stringify({ type: 'register_persona', persona: p }));\n }\n toast(`Persona \"${p.name}\" saved`, 'success');\n };\n\n const handleDeletePersona = (id: string) => {\n if (DEFAULT_PERSONAS.find(p => p.id === id)) { toast('Cannot delete built-in persona', 'error'); return; }\n deleteCustomPersona(id);\n setPersonas(loadPersonas());\n if (persona === id) handlePersonaChange('nexus');\n toast('Persona deleted', 'info');\n };\n\n const handleSaveModel = () => {\n localStorage.setItem('settings_model', model);\n if (apiKey) localStorage.setItem('settings_api_key', apiKey);\n else localStorage.removeItem('settings_api_key');\n toast('Model & API key saved', 'success');\n };\n\n const handleLanguage = (lang: string) => {\n setLanguage(lang);\n localStorage.setItem('settings_language', lang);\n toast(`Language: ${lang}`, 'success');\n };\n\n return (\n <div style={{ overflowY: 'auto', padding: '24px 32px', display: 'flex', flexDirection: 'column', gap: 24, height: '100%', maxWidth: 700 }}>\n <div>\n <h1 className=\"font-display\" style={{ fontSize: 44, letterSpacing: 6, color: 'var(--ink)', lineHeight: 1 }}>\n SETTINGS\n </h1>\n <p className=\"font-mono\" style={{ fontSize: 10, color: 'var(--dim)', letterSpacing: 1, marginTop: 4 }}>\n // SYSTEM CONFIGURATION\n </p>\n </div>\n\n {/* LLM Model + API Key — has save button */}\n <SettingSection icon={Cpu} title=\"LLM MODEL\">\n <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>\n {LLM_MODELS.map(m => (\n <label key={m.value} style={{\n display: 'flex', alignItems: 'center', gap: 10, padding: '10px 14px',\n border: `2px solid ${model === m.value ? 'var(--orange)' : 'var(--line)'}`,\n background: model === m.value ? 'rgba(212, 82, 26, 0.06)' : 'transparent',\n cursor: 'pointer', transition: 'all 0.2s',\n }}>\n <input type=\"radio\" name=\"model\" value={m.value} checked={model === m.value}\n onChange={() => setModel(m.value)} style={{ accentColor: 'var(--orange)' }} />\n <div style={{ flex: 1 }}>\n <div style={{ fontSize: 13, fontWeight: 500 }}>{m.label}</div>\n <div className=\"font-mono\" style={{ fontSize: 10, color: 'var(--dim)' }}>{m.desc}</div>\n </div>\n </label>\n ))}\n </div>\n\n <div style={{ marginTop: 14 }}>\n <div className=\"font-mono\" style={{ fontSize: 9, color: 'var(--orange)', letterSpacing: 2, marginBottom: 6, textTransform: 'uppercase', display: 'flex', alignItems: 'center', gap: 6 }}>\n <Shield size={10} /> API Key (optional)\n </div>\n <div style={{ display: 'flex', gap: 8 }}>\n <Input type=\"password\" placeholder=\"sk-... (leave empty for server default)\"\n value={apiKey} onChange={e => setApiKey(e.target.value)} style={{ flex: 1 }} />\n <Button onClick={handleSaveModel} size=\"sm\">SAVE</Button>\n </div>\n </div>\n </SettingSection>\n\n {/* Language — instant save on click */}\n <SettingSection icon={Globe} title=\"LANGUAGE\">\n <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>\n {LANGUAGES.map(l => (\n <button key={l.value} onClick={() => handleLanguage(l.value)} className=\"font-mono\"\n style={{\n padding: '8px 16px', border: `2px solid ${language === l.value ? 'var(--orange)' : 'var(--line)'}`,\n background: language === l.value ? 'rgba(212, 82, 26, 0.06)' : 'var(--cream)',\n color: language === l.value ? 'var(--orange)' : 'var(--ink)',\n fontSize: 12, cursor: 'pointer', transition: 'all 0.2s',\n }}>\n {l.label}\n </button>\n ))}\n </div>\n </SettingSection>\n\n {/* Theme — instant */}\n <SettingSection icon={Palette} title=\"THEME\">\n <div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>\n <button className=\"font-mono\" style={{\n padding: '8px 16px', border: '2px solid var(--orange)',\n background: 'rgba(212, 82, 26, 0.06)', color: 'var(--orange)',\n fontSize: 12, cursor: 'default', textTransform: 'uppercase',\n }}>\n NEXUS WARM\n </button>\n <span className=\"font-mono\" style={{ fontSize: 10, color: 'var(--dim)' }}>\n Dark mode coming soon\n </span>\n </div>\n </SettingSection>\n\n {/* Bot Persona */}\n <SettingSection icon={User} title=\"BOT PERSONA\">\n <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>\n {personas.map(p => (\n <div key={p.id} style={{\n display: 'flex', alignItems: 'center', gap: 10, padding: '10px 14px',\n border: `2px solid ${persona === p.id ? 'var(--orange)' : 'var(--line)'}`,\n background: persona === p.id ? 'rgba(212, 82, 26, 0.06)' : 'transparent',\n cursor: 'pointer', transition: 'all 0.2s',\n }} onClick={() => handlePersonaChange(p.id)}>\n <input type=\"radio\" name=\"persona\" value={p.id} checked={persona === p.id}\n onChange={() => {}} style={{ accentColor: 'var(--orange)' }} />\n <div style={{ flex: 1 }}>\n <div style={{ fontSize: 13, fontWeight: 500 }}>{p.emoji} {p.name}</div>\n {p.role && <div className=\"font-mono\" style={{ fontSize: 10, color: 'var(--orange)' }}>{p.role}</div>}\n <div className=\"font-mono\" style={{ fontSize: 10, color: 'var(--dim)' }}>{p.personality}</div>\n </div>\n <div style={{ display: 'flex', gap: 4 }}>\n <button onClick={e => { e.stopPropagation(); setEditingPersona({ ...p }); setShowPersonaEditor(true); }}\n className=\"font-mono\" style={{ background: 'none', border: '1px solid var(--line)', padding: '2px 6px', fontSize: 9, cursor: 'pointer', color: 'var(--mid)' }}>\n EDIT\n </button>\n {!DEFAULT_PERSONAS.find(d => d.id === p.id) && (\n <button onClick={e => { e.stopPropagation(); handleDeletePersona(p.id); }}\n className=\"font-mono\" style={{ background: 'none', border: '1px solid var(--error)', padding: '2px 6px', fontSize: 9, cursor: 'pointer', color: 'var(--error)' }}>\n DEL\n </button>\n )}\n </div>\n </div>\n ))}\n </div>\n <button onClick={() => { setEditingPersona({ id: '', name: '', emoji: '🤖', personality: '', greeting: '' }); setShowPersonaEditor(true); }}\n className=\"font-mono\" style={{\n marginTop: 10, padding: '8px 14px', border: '2px dashed var(--line)',\n background: 'transparent', color: 'var(--orange)', fontSize: 11,\n cursor: 'pointer', width: '100%', textAlign: 'center',\n }}>\n + CREATE CUSTOM PERSONA\n </button>\n\n {/* Persona Editor */}\n {showPersonaEditor && editingPersona && (\n <div style={{\n marginTop: 12, padding: 14, border: '2px solid var(--orange)',\n background: 'rgba(212, 82, 26, 0.03)', display: 'flex', flexDirection: 'column', gap: 10,\n }}>\n <div className=\"font-display\" style={{ fontSize: 14, letterSpacing: 2 }}>\n {editingPersona.id ? 'EDIT PERSONA' : 'NEW PERSONA'}\n </div>\n <div style={{ display: 'flex', gap: 8 }}>\n <div style={{ flex: 0 }}>\n <div className=\"font-mono\" style={{ fontSize: 9, color: 'var(--dim)', marginBottom: 4 }}>EMOJI</div>\n <Input value={editingPersona.emoji} onChange={e => setEditingPersona({ ...editingPersona, emoji: e.target.value })}\n style={{ width: 50, textAlign: 'center', fontSize: 20 }} />\n </div>\n <div style={{ flex: 1 }}>\n <div className=\"font-mono\" style={{ fontSize: 9, color: 'var(--dim)', marginBottom: 4 }}>NAME</div>\n <Input value={editingPersona.name} onChange={e => setEditingPersona({ ...editingPersona, name: e.target.value })}\n placeholder=\"e.g. Clawford\" />\n </div>\n </div>\n <div>\n <div className=\"font-mono\" style={{ fontSize: 9, color: 'var(--dim)', marginBottom: 4 }}>ROLE / IDENTITY</div>\n <Input value={editingPersona.role || ''} onChange={e => setEditingPersona({ ...editingPersona, role: e.target.value })}\n placeholder=\"e.g. Senior Data Analyst, HR Manager, DevOps Engineer\" />\n </div>\n <div>\n <div className=\"font-mono\" style={{ fontSize: 9, color: 'var(--dim)', marginBottom: 4 }}>PERSONALITY</div>\n <Input value={editingPersona.personality} onChange={e => setEditingPersona({ ...editingPersona, personality: e.target.value })}\n placeholder=\"e.g. Professional yet witty, data-driven\" />\n </div>\n <div>\n <div className=\"font-mono\" style={{ fontSize: 9, color: 'var(--dim)', marginBottom: 4 }}>EXPERTISE</div>\n <Input value={editingPersona.expertise || ''} onChange={e => setEditingPersona({ ...editingPersona, expertise: e.target.value })}\n placeholder=\"e.g. Financial analysis, KPI tracking, budget planning\" />\n </div>\n <div>\n <div className=\"font-mono\" style={{ fontSize: 9, color: 'var(--dim)', marginBottom: 4 }}>GREETING</div>\n <Input value={editingPersona.greeting} onChange={e => setEditingPersona({ ...editingPersona, greeting: e.target.value })}\n placeholder=\"e.g. Hello! I'm Clawford 🦀\" />\n </div>\n <div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end' }}>\n <Button variant=\"outline\" size=\"sm\" onClick={() => { setShowPersonaEditor(false); setEditingPersona(null); }}>CANCEL</Button>\n <Button size=\"sm\" onClick={handleSavePersona}>SAVE</Button>\n </div>\n </div>\n )}\n </SettingSection>\n\n {/* Bot — instant toggle + slider */}\n <SettingSection icon={Bot} title=\"3D BOT\">\n <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>\n <label style={{ display: 'flex', alignItems: 'center', gap: 8, cursor: 'pointer' }}>\n <input type=\"checkbox\" checked={botEnabled} onChange={e => setBotEnabled(e.target.checked)}\n style={{ width: 18, height: 18, accentColor: 'var(--orange)' }} />\n <span style={{ fontSize: 13 }}>{botEnabled ? 'Enabled' : 'Disabled'}</span>\n </label>\n {botEnabled && (\n <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>\n <span className=\"font-mono\" style={{ fontSize: 10, color: 'var(--dim)', minWidth: 30 }}>Size</span>\n <input type=\"range\" min={80} max={260} value={botSize}\n onChange={e => setBotSize(Number(e.target.value))}\n style={{ flex: 1, maxWidth: 200, accentColor: 'var(--orange)' }} />\n <span className=\"font-mono\" style={{ fontSize: 10, color: 'var(--dim)', minWidth: 40 }}>{botSize}px</span>\n </div>\n )}\n </div>\n </SettingSection>\n </div>\n );\n}\n\nfunction SettingSection({ icon: Icon, title, children }: {\n icon: any; title: string; children: React.ReactNode;\n}) {\n return (\n <div className=\"animate-card-in\" style={{ border: '2px solid var(--ink)', padding: '18px 20px', background: 'var(--cream)' }}>\n <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 14 }}>\n <Icon size={16} color=\"var(--orange)\" />\n <span className=\"font-display\" style={{ fontSize: 16, letterSpacing: 3 }}>{title}</span>\n </div>\n {children}\n </div>\n );\n}\n"}, "9af26057511ca6a4413d0db8e747b4cfd31f11fe": {"t": 1785749331.7473958, "n": 16209, "h": "b96c5a401110c1d5", "c": "import { useState } from 'react';\nimport { useBotStore } from '../../store/bot';\nimport { useThemeStore } from '../../store/theme';\nimport { Button } from '../../components/ui/Button';\nimport { Input } from '../../components/ui/Input';\nimport { toast } from '../../components/ui/Toast';\nimport { Bot, Cpu, Palette, Globe, Shield, User } from 'lucide-react';\n\nconst LLM_MODELS = [\n { value: 'deepseek/deepseek-chat', label: 'DeepSeek Chat', desc: 'Fast & cheap' },\n { value: 'openai/gpt-4o', label: 'GPT-4o', desc: 'Popular' },\n { value: 'anthropic/claude-sonnet-4-20250514', label: 'Claude Sonnet', desc: 'Best quality' },\n { value: 'ollama/llama3', label: 'Ollama Llama3', desc: 'Free, local' },\n { value: 'ollama/qwen2.5', label: 'Ollama Qwen2.5', desc: 'Free, local, Chinese' },\n];\n\nconst LANGUAGES = [\n { value: 'auto', label: 'Auto (match user)' },\n { value: 'en', label: 'English' },\n { value: 'zh', label: '中文' },\n { value: 'ja', label: '日本語' },\n];\n\nconst DEFAULT_PERSONAS = [\n { id: 'clawford', name: 'Clawford', emoji: '🦀', role: 'Senior Data Analyst', personality: 'Professional yet witty, data-driven with a pinch of humor', expertise: 'Financial analysis, business intelligence, KPI tracking', greeting: \"Hello! I'm Clawford, your enterprise data analyst 🦀 Ready to crunch some numbers?\" },\n { id: 'nexus', name: 'Nexus', emoji: '⚡', role: 'System Operations Engineer', personality: 'Strictly professional, concise, precise', expertise: 'System monitoring, DevOps, performance optimization', greeting: \"Nexus online. All systems nominal.\" },\n { id: 'buddy', name: 'Buddy', emoji: '🎉', role: 'General Assistant', personality: 'Casual, talkative, friendly, lots of emoji', expertise: 'General Q&A, onboarding, feature explanation', greeting: \"Hey there! What's up? 🎉\" },\n];\n\nfunction loadPersonas() {\n const custom = localStorage.getItem('custom_personas');\n const customs = custom ? JSON.parse(custom) : [];\n return [...DEFAULT_PERSONAS, ...customs];\n}\n\nfunction saveCustomPersona(p: { id: string; name: string; emoji: string; personality: string; greeting: string }) {\n const custom = localStorage.getItem('custom_personas');\n const customs = custom ? JSON.parse(custom) : [];\n const existing = customs.findIndex((c: any) => c.id === p.id);\n if (existing >= 0) customs[existing] = p;\n else customs.push(p);\n localStorage.setItem('custom_personas', JSON.stringify(customs));\n}\n\nfunction deleteCustomPersona(id: string) {\n const custom = localStorage.getItem('custom_personas');\n const customs = custom ? JSON.parse(custom) : [];\n localStorage.setItem('custom_personas', JSON.stringify(customs.filter((c: any) => c.id !== id)));\n}\n\nexport default function SettingsPage() {\n const { enabled: botEnabled, setEnabled: setBotEnabled, size: botSize, setSize: setBotSize } = useBotStore();\n const { theme } = useThemeStore();\n\n const [model, setModel] = useState(() => localStorage.getItem('settings_model') || 'deepseek/deepseek-chat');\n const [apiKey, setApiKey] = useState(() => localStorage.getItem('settings_api_key') || '');\n const [language, setLanguage] = useState(() => localStorage.getItem('settings_language') || 'auto');\n const [persona, setPersona] = useState(() => localStorage.getItem('settings_persona') || 'clawford');\n const [personas, setPersonas] = useState(loadPersonas);\n const [editingPersona, setEditingPersona] = useState<any>(null);\n const [showPersonaEditor, setShowPersonaEditor] = useState(false);\n\n const handlePersonaChange = (id: string) => {\n setPersona(id);\n localStorage.setItem('settings_persona', id);\n const ws = (window as any).__botWs;\n if (ws?.readyState === WebSocket.OPEN) {\n ws.send(JSON.stringify({ type: 'persona_change', persona: id }));\n }\n toast(`Persona: ${personas.find(p => p.id === id)?.name}`, 'success');\n };\n\n const handleSavePersona = () => {\n if (!editingPersona?.name) return;\n const p = { ...editingPersona, id: editingPersona.id || `custom_${Date.now()}` };\n saveCustomPersona(p);\n setPersonas(loadPersonas());\n setShowPersonaEditor(false);\n setEditingPersona(null);\n // Also send to backend to register\n const ws = (window as any).__botWs;\n if (ws?.readyState === WebSocket.OPEN) {\n ws.send(JSON.stringify({ type: 'register_persona', persona: p }));\n }\n toast(`Persona \"${p.name}\" saved`, 'success');\n };\n\n const handleDeletePersona = (id: string) => {\n if (DEFAULT_PERSONAS.find(p => p.id === id)) { toast('Cannot delete built-in persona', 'error'); return; }\n deleteCustomPersona(id);\n setPersonas(loadPersonas());\n if (persona === id) handlePersonaChange('nexus');\n toast('Persona deleted', 'info');\n };\n\n const handleSaveModel = () => {\n localStorage.setItem('settings_model', model);\n if (apiKey) localStorage.setItem('settings_api_key', apiKey);\n else localStorage.removeItem('settings_api_key');\n toast('Model & API key saved', 'success');\n };\n\n const handleLanguage = (lang: string) => {\n setLanguage(lang);\n localStorage.setItem('settings_language', lang);\n toast(`Language: ${lang}`, 'success');\n };\n\n return (\n <div style={{ overflowY: 'auto', padding: '24px 32px', display: 'flex', flexDirection: 'column', gap: 24, height: '100%', maxWidth: 700 }}>\n <div>\n <h1 className=\"font-display\" style={{ fontSize: 44, letterSpacing: 6, color: 'var(--ink)', lineHeight: 1 }}>\n SETTINGS\n </h1>\n <p className=\"font-mono\" style={{ fontSize: 10, color: 'var(--dim)', letterSpacing: 1, marginTop: 4 }}>\n // SYSTEM CONFIGURATION\n </p>\n </div>\n\n {/* LLM Model + API Key — has save button */}\n <SettingSection icon={Cpu} title=\"LLM MODEL\">\n <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>\n {LLM_MODELS.map(m => (\n <label key={m.value} style={{\n display: 'flex', alignItems: 'center', gap: 10, padding: '10px 14px',\n border: `2px solid ${model === m.value ? 'var(--orange)' : 'var(--line)'}`,\n background: model === m.value ? 'rgba(212, 82, 26, 0.06)' : 'transparent',\n cursor: 'pointer', transition: 'all 0.2s',\n }}>\n <input type=\"radio\" name=\"model\" value={m.value} checked={model === m.value}\n onChange={() => setModel(m.value)} style={{ accentColor: 'var(--orange)' }} />\n <div style={{ flex: 1 }}>\n <div style={{ fontSize: 13, fontWeight: 500 }}>{m.label}</div>\n <div className=\"font-mono\" style={{ fontSize: 10, color: 'var(--dim)' }}>{m.desc}</div>\n </div>\n </label>\n ))}\n </div>\n\n <div style={{ marginTop: 14 }}>\n <div className=\"font-mono\" style={{ fontSize: 9, color: 'var(--orange)', letterSpacing: 2, marginBottom: 6, textTransform: 'uppercase', display: 'flex', alignItems: 'center', gap: 6 }}>\n <Shield size={10} /> API Key (optional)\n </div>\n <div style={{ display: 'flex', gap: 8 }}>\n <Input type=\"password\" placeholder=\"sk-... (leave empty for server default)\"\n value={apiKey} onChange={e => setApiKey(e.target.value)} style={{ flex: 1 }} />\n <Button onClick={handleSaveModel} size=\"sm\">SAVE</Button>\n </div>\n </div>\n </SettingSection>\n\n {/* Language — instant save on click */}\n <SettingSection icon={Globe} title=\"LANGUAGE\">\n <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>\n {LANGUAGES.map(l => (\n <button key={l.value} onClick={() => handleLanguage(l.value)} className=\"font-mono\"\n style={{\n padding: '8px 16px', border: `2px solid ${language === l.value ? 'var(--orange)' : 'var(--line)'}`,\n background: language === l.value ? 'rgba(212, 82, 26, 0.06)' : 'var(--cream)',\n color: language === l.value ? 'var(--orange)' : 'var(--ink)',\n fontSize: 12, cursor: 'pointer', transition: 'all 0.2s',\n }}>\n {l.label}\n </button>\n ))}\n </div>\n </SettingSection>\n\n {/* Theme — instant */}\n <SettingSection icon={Palette} title=\"THEME\">\n <div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>\n <button className=\"font-mono\" style={{\n padding: '8px 16px', border: '2px solid var(--orange)',\n background: 'rgba(212, 82, 26, 0.06)', color: 'var(--orange)',\n fontSize: 12, cursor: 'default', textTransform: 'uppercase',\n }}>\n NEXUS WARM\n </button>\n <span className=\"font-mono\" style={{ fontSize: 10, color: 'var(--dim)' }}>\n Dark mode coming soon\n </span>\n </div>\n </SettingSection>\n\n {/* Bot Persona */}\n <SettingSection icon={User} title=\"BOT PERSONA\">\n <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>\n {personas.map(p => (\n <div key={p.id} style={{\n display: 'flex', alignItems: 'center', gap: 10, padding: '10px 14px',\n border: `2px solid ${persona === p.id ? 'var(--orange)' : 'var(--line)'}`,\n background: persona === p.id ? 'rgba(212, 82, 26, 0.06)' : 'transparent',\n cursor: 'pointer', transition: 'all 0.2s',\n }} onClick={() => handlePersonaChange(p.id)}>\n <input type=\"radio\" name=\"persona\" value={p.id} checked={persona === p.id}\n onChange={() => {}} style={{ accentColor: 'var(--orange)' }} />\n <div style={{ flex: 1 }}>\n <div style={{ fontSize: 13, fontWeight: 500 }}>{p.emoji} {p.name}</div>\n {p.role && <div className=\"font-mono\" style={{ fontSize: 10, color: 'var(--orange)' }}>{p.role}</div>}\n <div className=\"font-mono\" style={{ fontSize: 10, color: 'var(--dim)' }}>{p.personality}</div>\n </div>\n <div style={{ display: 'flex', gap: 4 }}>\n <button onClick={e => { e.stopPropagation(); setEditingPersona({ ...p }); setShowPersonaEditor(true); }}\n className=\"font-mono\" style={{ background: 'none', border: '1px solid var(--line)', padding: '2px 6px', fontSize: 9, cursor: 'pointer', color: 'var(--mid)' }}>\n EDIT\n </button>\n {!DEFAULT_PERSONAS.find(d => d.id === p.id) && (\n <button onClick={e => { e.stopPropagation(); handleDeletePersona(p.id); }}\n className=\"font-mono\" style={{ background: 'none', border: '1px solid var(--error)', padding: '2px 6px', fontSize: 9, cursor: 'pointer', color: 'var(--error)' }}>\n DEL\n </button>\n )}\n </div>\n </div>\n ))}\n </div>\n <button onClick={() => { setEditingPersona({ id: '', name: '', emoji: '🤖', personality: '', greeting: '' }); setShowPersonaEditor(true); }}\n className=\"font-mono\" style={{\n marginTop: 10, padding: '8px 14px', border: '2px dashed var(--line)',\n background: 'transparent', color: 'var(--orange)', fontSize: 11,\n cursor: 'pointer', width: '100%', textAlign: 'center',\n }}>\n + CREATE CUSTOM PERSONA\n </button>\n\n {/* Persona Editor */}\n {showPersonaEditor && editingPersona && (\n <div style={{\n marginTop: 12, padding: 14, border: '2px solid var(--orange)',\n background: 'rgba(212, 82, 26, 0.03)', display: 'flex', flexDirection: 'column', gap: 10,\n }}>\n <div className=\"font-display\" style={{ fontSize: 14, letterSpacing: 2 }}>\n {editingPersona.id ? 'EDIT PERSONA' : 'NEW PERSONA'}\n </div>\n <div style={{ display: 'flex', gap: 8 }}>\n <div style={{ flex: 0 }}>\n <div className=\"font-mono\" style={{ fontSize: 9, color: 'var(--dim)', marginBottom: 4 }}>EMOJI</div>\n <Input value={editingPersona.emoji} onChange={e => setEditingPersona({ ...editingPersona, emoji: e.target.value })}\n style={{ width: 50, textAlign: 'center', fontSize: 20 }} />\n </div>\n <div style={{ flex: 1 }}>\n <div className=\"font-mono\" style={{ fontSize: 9, color: 'var(--dim)', marginBottom: 4 }}>NAME</div>\n <Input value={editingPersona.name} onChange={e => setEditingPersona({ ...editingPersona, name: e.target.value })}\n placeholder=\"e.g. Clawford\" />\n </div>\n </div>\n <div>\n <div className=\"font-mono\" style={{ fontSize: 9, color: 'var(--dim)', marginBottom: 4 }}>ROLE / IDENTITY</div>\n <Input value={editingPersona.role || ''} onChange={e => setEditingPersona({ ...editingPersona, role: e.target.value })}\n placeholder=\"e.g. Senior Data Analyst, HR Manager, DevOps Engineer\" />\n </div>\n <div>\n <div className=\"font-mono\" style={{ fontSize: 9, color: 'var(--dim)', marginBottom: 4 }}>PERSONALITY</div>\n <Input value={editingPersona.personality} onChange={e => setEditingPersona({ ...editingPersona, personality: e.target.value })}\n placeholder=\"e.g. Professional yet witty, data-driven\" />\n </div>\n <div>\n <div className=\"font-mono\" style={{ fontSize: 9, color: 'var(--dim)', marginBottom: 4 }}>EXPERTISE</div>\n <Input value={editingPersona.expertise || ''} onChange={e => setEditingPersona({ ...editingPersona, expertise: e.target.value })}\n placeholder=\"e.g. Financial analysis, KPI tracking, budget planning\" />\n </div>\n <div>\n <div className=\"font-mono\" style={{ fontSize: 9, color: 'var(--dim)', marginBottom: 4 }}>GREETING</div>\n <Input value={editingPersona.greeting} onChange={e => setEditingPersona({ ...editingPersona, greeting: e.target.value })}\n placeholder=\"e.g. Hello! I'm Clawford 🦀\" />\n </div>\n <div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end' }}>\n <Button variant=\"outline\" size=\"sm\" onClick={() => { setShowPersonaEditor(false); setEditingPersona(null); }}>CANCEL</Button>\n <Button size=\"sm\" onClick={handleSavePersona}>SAVE</Button>\n </div>\n </div>\n )}\n </SettingSection>\n\n {/* Bot — instant toggle + slider */}\n <SettingSection icon={Bot} title=\"3D BOT\">\n <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>\n <label style={{ display: 'flex', alignItems: 'center', gap: 8, cursor: 'pointer' }}>\n <input type=\"checkbox\" checked={botEnabled} onChange={e => setBotEnabled(e.target.checked)}\n style={{ width: 18, height: 18, accentColor: 'var(--orange)' }} />\n <span style={{ fontSize: 13 }}>{botEnabled ? 'Enabled' : 'Disabled'}</span>\n </label>\n {botEnabled && (\n <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>\n <span className=\"font-mono\" style={{ fontSize: 10, color: 'var(--dim)', minWidth: 30 }}>Size</span>\n <input type=\"range\" min={80} max={260} value={botSize}\n onChange={e => setBotSize(Number(e.target.value))}\n style={{ flex: 1, maxWidth: 200, accentColor: 'var(--orange)' }} />\n <span className=\"font-mono\" style={{ fontSize: 10, color: 'var(--dim)', minWidth: 40 }}>{botSize}px</span>\n </div>\n )}\n </div>\n </SettingSection>\n </div>\n );\n}\n\nfunction SettingSection({ icon: Icon, title, children }: {\n icon: any; title: string; children: React.ReactNode;\n}) {\n return (\n <div className=\"animate-card-in\" style={{ border: '2px solid var(--ink)', padding: '18px 20px', background: 'var(--cream)' }}>\n <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 14 }}>\n <Icon size={16} color=\"var(--orange)\" />\n <span className=\"font-display\" style={{ fontSize: 16, letterSpacing: 3 }}>{title}</span>\n </div>\n {children}\n </div>\n );\n}\n"}, "7e3fc22dc06892786a455623e12e775d554a2662": {"t": 1785749332.873869, "n": 15017, "h": "f5254fc8cff0bebe", "c": "import { useState } from 'react';\nimport { useBotStore } from '../../store/bot';\nimport { useThemeStore } from '../../store/theme';\nimport { Button } from '../../components/ui/Button';\nimport { Input } from '../../components/ui/Input';\nimport { toast } from '../../components/ui/Toast';\nimport { Bot, Cpu, Palette, Globe, Shield, User } from 'lucide-react';\n\nconst LLM_MODELS = [\n { value: 'deepseek/deepseek-chat', label: 'DeepSeek Chat', desc: 'Fast & cheap' },\n { value: 'openai/gpt-4o', label: 'GPT-4o', desc: 'Popular' },\n { value: 'anthropic/claude-sonnet-4-20250514', label: 'Claude Sonnet', desc: 'Best quality' },\n { value: 'ollama/llama3', label: 'Ollama Llama3', desc: 'Free, local' },\n { value: 'ollama/qwen2.5', label: 'Ollama Qwen2.5', desc: 'Free, local, Chinese' },\n];\n\nconst LANGUAGES = [\n { value: 'auto', label: 'Auto (match user)' },\n { value: 'en', label: 'English' },\n { value: 'zh', label: '中文' },\n { value: 'ja', label: '日本語' },\n];\n\nconst DEFAULT_PERSONAS = [\n { id: 'clawford', name: 'Clawford', emoji: '🦀', personality: 'Professional yet witty, enterprise-grade with a pinch of humor', greeting: \"Hello! I'm Clawford, your enterprise AI assistant 🦀 How can I help?\" },\n { id: 'nexus', name: 'Nexus', emoji: '⚡', personality: 'Strictly professional, concise, data-driven', greeting: \"Nexus online. Ready for your query.\" },\n { id: 'buddy', name: 'Buddy', emoji: '🎉', personality: 'Casual, talkative, lots of emoji, friendly', greeting: \"Hey there! What's up? 🎉\" },\n];\n\nfunction loadPersonas() {\n const custom = localStorage.getItem('custom_personas');\n const customs = custom ? JSON.parse(custom) : [];\n return [...DEFAULT_PERSONAS, ...customs];\n}\n\nfunction saveCustomPersona(p: { id: string; name: string; emoji: string; personality: string; greeting: string }) {\n const custom = localStorage.getItem('custom_personas');\n const customs = custom ? JSON.parse(custom) : [];\n const existing = customs.findIndex((c: any) => c.id === p.id);\n if (existing >= 0) customs[existing] = p;\n else customs.push(p);\n localStorage.setItem('custom_personas', JSON.stringify(customs));\n}\n\nfunction deleteCustomPersona(id: string) {\n const custom = localStorage.getItem('custom_personas');\n const customs = custom ? JSON.parse(custom) : [];\n localStorage.setItem('custom_personas', JSON.stringify(customs.filter((c: any) => c.id !== id)));\n}\n\nexport default function SettingsPage() {\n const { enabled: botEnabled, setEnabled: setBotEnabled, size: botSize, setSize: setBotSize } = useBotStore();\n const { theme } = useThemeStore();\n\n const [model, setModel] = useState(() => localStorage.getItem('settings_model') || 'deepseek/deepseek-chat');\n const [apiKey, setApiKey] = useState(() => localStorage.getItem('settings_api_key') || '');\n const [language, setLanguage] = useState(() => localStorage.getItem('settings_language') || 'auto');\n const [persona, setPersona] = useState(() => localStorage.getItem('settings_persona') || 'clawford');\n const [personas, setPersonas] = useState(loadPersonas);\n const [editingPersona, setEditingPersona] = useState<any>(null);\n const [showPersonaEditor, setShowPersonaEditor] = useState(false);\n\n const handlePersonaChange = (id: string) => {\n setPersona(id);\n localStorage.setItem('settings_persona', id);\n const ws = (window as any).__botWs;\n if (ws?.readyState === WebSocket.OPEN) {\n ws.send(JSON.stringify({ type: 'persona_change', persona: id }));\n }\n toast(`Persona: ${personas.find(p => p.id === id)?.name}`, 'success');\n };\n\n const handleSavePersona = () => {\n if (!editingPersona?.name) return;\n const p = { ...editingPersona, id: editingPersona.id || `custom_${Date.now()}` };\n saveCustomPersona(p);\n setPersonas(loadPersonas());\n setShowPersonaEditor(false);\n setEditingPersona(null);\n // Also send to backend to register\n const ws = (window as any).__botWs;\n if (ws?.readyState === WebSocket.OPEN) {\n ws.send(JSON.stringify({ type: 'register_persona', persona: p }));\n }\n toast(`Persona \"${p.name}\" saved`, 'success');\n };\n\n const handleDeletePersona = (id: string) => {\n if (DEFAULT_PERSONAS.find(p => p.id === id)) { toast('Cannot delete built-in persona', 'error'); return; }\n deleteCustomPersona(id);\n setPersonas(loadPersonas());\n if (persona === id) handlePersonaChange('nexus');\n toast('Persona deleted', 'info');\n };\n\n const handleSaveModel = () => {\n localStorage.setItem('settings_model', model);\n if (apiKey) localStorage.setItem('settings_api_key', apiKey);\n else localStorage.removeItem('settings_api_key');\n toast('Model & API key saved', 'success');\n };\n\n const handleLanguage = (lang: string) => {\n setLanguage(lang);\n localStorage.setItem('settings_language', lang);\n toast(`Language: ${lang}`, 'success');\n };\n\n return (\n <div style={{ overflowY: 'auto', padding: '24px 32px', display: 'flex', flexDirection: 'column', gap: 24, height: '100%', maxWidth: 700 }}>\n <div>\n <h1 className=\"font-display\" style={{ fontSize: 44, letterSpacing: 6, color: 'var(--ink)', lineHeight: 1 }}>\n SETTINGS\n </h1>\n <p className=\"font-mono\" style={{ fontSize: 10, color: 'var(--dim)', letterSpacing: 1, marginTop: 4 }}>\n // SYSTEM CONFIGURATION\n </p>\n </div>\n\n {/* LLM Model + API Key — has save button */}\n <SettingSection icon={Cpu} title=\"LLM MODEL\">\n <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>\n {LLM_MODELS.map(m => (\n <label key={m.value} style={{\n display: 'flex', alignItems: 'center', gap: 10, padding: '10px 14px',\n border: `2px solid ${model === m.value ? 'var(--orange)' : 'var(--line)'}`,\n background: model === m.value ? 'rgba(212, 82, 26, 0.06)' : 'transparent',\n cursor: 'pointer', transition: 'all 0.2s',\n }}>\n <input type=\"radio\" name=\"model\" value={m.value} checked={model === m.value}\n onChange={() => setModel(m.value)} style={{ accentColor: 'var(--orange)' }} />\n <div style={{ flex: 1 }}>\n <div style={{ fontSize: 13, fontWeight: 500 }}>{m.label}</div>\n <div className=\"font-mono\" style={{ fontSize: 10, color: 'var(--dim)' }}>{m.desc}</div>\n </div>\n </label>\n ))}\n </div>\n\n <div style={{ marginTop: 14 }}>\n <div className=\"font-mono\" style={{ fontSize: 9, color: 'var(--orange)', letterSpacing: 2, marginBottom: 6, textTransform: 'uppercase', display: 'flex', alignItems: 'center', gap: 6 }}>\n <Shield size={10} /> API Key (optional)\n </div>\n <div style={{ display: 'flex', gap: 8 }}>\n <Input type=\"password\" placeholder=\"sk-... (leave empty for server default)\"\n value={apiKey} onChange={e => setApiKey(e.target.value)} style={{ flex: 1 }} />\n <Button onClick={handleSaveModel} size=\"sm\">SAVE</Button>\n </div>\n </div>\n </SettingSection>\n\n {/* Language — instant save on click */}\n <SettingSection icon={Globe} title=\"LANGUAGE\">\n <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>\n {LANGUAGES.map(l => (\n <button key={l.value} onClick={() => handleLanguage(l.value)} className=\"font-mono\"\n style={{\n padding: '8px 16px', border: `2px solid ${language === l.value ? 'var(--orange)' : 'var(--line)'}`,\n background: language === l.value ? 'rgba(212, 82, 26, 0.06)' : 'var(--cream)',\n color: language === l.value ? 'var(--orange)' : 'var(--ink)',\n fontSize: 12, cursor: 'pointer', transition: 'all 0.2s',\n }}>\n {l.label}\n </button>\n ))}\n </div>\n </SettingSection>\n\n {/* Theme — instant */}\n <SettingSection icon={Palette} title=\"THEME\">\n <div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>\n <button className=\"font-mono\" style={{\n padding: '8px 16px', border: '2px solid var(--orange)',\n background: 'rgba(212, 82, 26, 0.06)', color: 'var(--orange)',\n fontSize: 12, cursor: 'default', textTransform: 'uppercase',\n }}>\n NEXUS WARM\n </button>\n <span className=\"font-mono\" style={{ fontSize: 10, color: 'var(--dim)' }}>\n Dark mode coming soon\n </span>\n </div>\n </SettingSection>\n\n {/* Bot Persona */}\n <SettingSection icon={User} title=\"BOT PERSONA\">\n <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>\n {personas.map(p => (\n <div key={p.id} style={{\n display: 'flex', alignItems: 'center', gap: 10, padding: '10px 14px',\n border: `2px solid ${persona === p.id ? 'var(--orange)' : 'var(--line)'}`,\n background: persona === p.id ? 'rgba(212, 82, 26, 0.06)' : 'transparent',\n cursor: 'pointer', transition: 'all 0.2s',\n }} onClick={() => handlePersonaChange(p.id)}>\n <input type=\"radio\" name=\"persona\" value={p.id} checked={persona === p.id}\n onChange={() => {}} style={{ accentColor: 'var(--orange)' }} />\n <div style={{ flex: 1 }}>\n <div style={{ fontSize: 13, fontWeight: 500 }}>{p.emoji} {p.name}</div>\n <div className=\"font-mono\" style={{ fontSize: 10, color: 'var(--dim)' }}>{p.personality}</div>\n </div>\n <div style={{ display: 'flex', gap: 4 }}>\n <button onClick={e => { e.stopPropagation(); setEditingPersona({ ...p }); setShowPersonaEditor(true); }}\n className=\"font-mono\" style={{ background: 'none', border: '1px solid var(--line)', padding: '2px 6px', fontSize: 9, cursor: 'pointer', color: 'var(--mid)' }}>\n EDIT\n </button>\n {!DEFAULT_PERSONAS.find(d => d.id === p.id) && (\n <button onClick={e => { e.stopPropagation(); handleDeletePersona(p.id); }}\n className=\"font-mono\" style={{ background: 'none', border: '1px solid var(--error)', padding: '2px 6px', fontSize: 9, cursor: 'pointer', color: 'var(--error)' }}>\n DEL\n </button>\n )}\n </div>\n </div>\n ))}\n </div>\n <button onClick={() => { setEditingPersona({ id: '', name: '', emoji: '🤖', personality: '', greeting: '' }); setShowPersonaEditor(true); }}\n className=\"font-mono\" style={{\n marginTop: 10, padding: '8px 14px', border: '2px dashed var(--line)',\n background: 'transparent', color: 'var(--orange)', fontSize: 11,\n cursor: 'pointer', width: '100%', textAlign: 'center',\n }}>\n + CREATE CUSTOM PERSONA\n </button>\n\n {/* Persona Editor */}\n {showPersonaEditor && editingPersona && (\n <div style={{\n marginTop: 12, padding: 14, border: '2px solid var(--orange)',\n background: 'rgba(212, 82, 26, 0.03)', display: 'flex', flexDirection: 'column', gap: 10,\n }}>\n <div className=\"font-display\" style={{ fontSize: 14, letterSpacing: 2 }}>\n {editingPersona.id ? 'EDIT PERSONA' : 'NEW PERSONA'}\n </div>\n <div style={{ display: 'flex', gap: 8 }}>\n <div style={{ flex: 0 }}>\n <div className=\"font-mono\" style={{ fontSize: 9, color: 'var(--dim)', marginBottom: 4 }}>EMOJI</div>\n <Input value={editingPersona.emoji} onChange={e => setEditingPersona({ ...editingPersona, emoji: e.target.value })}\n style={{ width: 50, textAlign: 'center', fontSize: 20 }} />\n </div>\n <div style={{ flex: 1 }}>\n <div className=\"font-mono\" style={{ fontSize: 9, color: 'var(--dim)', marginBottom: 4 }}>NAME</div>\n <Input value={editingPersona.name} onChange={e => setEditingPersona({ ...editingPersona, name: e.target.value })}\n placeholder=\"e.g. 蟹老板\" />\n </div>\n </div>\n <div>\n <div className=\"font-mono\" style={{ fontSize: 9, color: 'var(--dim)', marginBottom: 4 }}>PERSONALITY</div>\n <Input value={editingPersona.personality} onChange={e => setEditingPersona({ ...editingPersona, personality: e.target.value })}\n placeholder=\"e.g. Cheerful, humorous, speaks with crab puns\" />\n </div>\n <div>\n <div className=\"font-mono\" style={{ fontSize: 9, color: 'var(--dim)', marginBottom: 4 }}>GREETING</div>\n <Input value={editingPersona.greeting} onChange={e => setEditingPersona({ ...editingPersona, greeting: e.target.value })}\n placeholder=\"e.g. 嘿!我是蟹老板 🦀\" />\n </div>\n <div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end' }}>\n <Button variant=\"outline\" size=\"sm\" onClick={() => { setShowPersonaEditor(false); setEditingPersona(null); }}>CANCEL</Button>\n <Button size=\"sm\" onClick={handleSavePersona}>SAVE</Button>\n </div>\n </div>\n )}\n </SettingSection>\n\n {/* Bot — instant toggle + slider */}\n <SettingSection icon={Bot} title=\"3D BOT\">\n <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>\n <label style={{ display: 'flex', alignItems: 'center', gap: 8, cursor: 'pointer' }}>\n <input type=\"checkbox\" checked={botEnabled} onChange={e => setBotEnabled(e.target.checked)}\n style={{ width: 18, height: 18, accentColor: 'var(--orange)' }} />\n <span style={{ fontSize: 13 }}>{botEnabled ? 'Enabled' : 'Disabled'}</span>\n </label>\n {botEnabled && (\n <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>\n <span className=\"font-mono\" style={{ fontSize: 10, color: 'var(--dim)', minWidth: 30 }}>Size</span>\n <input type=\"range\" min={80} max={260} value={botSize}\n onChange={e => setBotSize(Number(e.target.value))}\n style={{ flex: 1, maxWidth: 200, accentColor: 'var(--orange)' }} />\n <span className=\"font-mono\" style={{ fontSize: 10, color: 'var(--dim)', minWidth: 40 }}>{botSize}px</span>\n </div>\n )}\n </div>\n </SettingSection>\n </div>\n );\n}\n\nfunction SettingSection({ icon: Icon, title, children }: {\n icon: any; title: string; children: React.ReactNode;\n}) {\n return (\n <div className=\"animate-card-in\" style={{ border: '2px solid var(--ink)', padding: '18px 20px', background: 'var(--cream)' }}>\n <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 14 }}>\n <Icon size={16} color=\"var(--orange)\" />\n <span className=\"font-display\" style={{ fontSize: 16, letterSpacing: 3 }}>{title}</span>\n </div>\n {children}\n </div>\n );\n}\n"}, "6d84a5b42f8bfec9facae2a29489c07d809c414e": {"t": 1785749334.0506852, "n": 14929, "h": "0d806da65799d1a8", "c": "import { useState } from 'react';\nimport { useBotStore } from '../../store/bot';\nimport { useThemeStore } from '../../store/theme';\nimport { Button } from '../../components/ui/Button';\nimport { Input } from '../../components/ui/Input';\nimport { toast } from '../../components/ui/Toast';\nimport { Bot, Cpu, Palette, Globe, Shield, User } from 'lucide-react';\n\nconst LLM_MODELS = [\n { value: 'deepseek/deepseek-chat', label: 'DeepSeek Chat', desc: 'Fast & cheap' },\n { value: 'openai/gpt-4o', label: 'GPT-4o', desc: 'Popular' },\n { value: 'anthropic/claude-sonnet-4-20250514', label: 'Claude Sonnet', desc: 'Best quality' },\n { value: 'ollama/llama3', label: 'Ollama Llama3', desc: 'Free, local' },\n { value: 'ollama/qwen2.5', label: 'Ollama Qwen2.5', desc: 'Free, local, Chinese' },\n];\n\nconst LANGUAGES = [\n { value: 'auto', label: 'Auto (match user)' },\n { value: 'en', label: 'English' },\n { value: 'zh', label: '中文' },\n { value: 'ja', label: '日本語' },\n];\n\nconst DEFAULT_PERSONAS = [\n { id: 'crab_boss', name: '蟹老板', emoji: '🦀', personality: 'Cheerful, humorous, business-minded crab', greeting: '嘿!我是蟹老板 🦀 有什么需要我帮忙的吗?' },\n { id: 'nexus', name: 'Nexus', emoji: '🤖', personality: 'Professional, concise, friendly assistant', greeting: \"Hi! I'm Nexus. How can I help?\" },\n { id: 'buddy', name: 'Buddy', emoji: '🎉', personality: 'Casual, talkative, lots of emoji', greeting: \"Hey there! What's up? 🎉\" },\n];\n\nfunction loadPersonas() {\n const custom = localStorage.getItem('custom_personas');\n const customs = custom ? JSON.parse(custom) : [];\n return [...DEFAULT_PERSONAS, ...customs];\n}\n\nfunction saveCustomPersona(p: { id: string; name: string; emoji: string; personality: string; greeting: string }) {\n const custom = localStorage.getItem('custom_personas');\n const customs = custom ? JSON.parse(custom) : [];\n const existing = customs.findIndex((c: any) => c.id === p.id);\n if (existing >= 0) customs[existing] = p;\n else customs.push(p);\n localStorage.setItem('custom_personas', JSON.stringify(customs));\n}\n\nfunction deleteCustomPersona(id: string) {\n const custom = localStorage.getItem('custom_personas');\n const customs = custom ? JSON.parse(custom) : [];\n localStorage.setItem('custom_personas', JSON.stringify(customs.filter((c: any) => c.id !== id)));\n}\n\nexport default function SettingsPage() {\n const { enabled: botEnabled, setEnabled: setBotEnabled, size: botSize, setSize: setBotSize } = useBotStore();\n const { theme } = useThemeStore();\n\n const [model, setModel] = useState(() => localStorage.getItem('settings_model') || 'deepseek/deepseek-chat');\n const [apiKey, setApiKey] = useState(() => localStorage.getItem('settings_api_key') || '');\n const [language, setLanguage] = useState(() => localStorage.getItem('settings_language') || 'auto');\n const [persona, setPersona] = useState(() => localStorage.getItem('settings_persona') || 'crab_boss');\n const [personas, setPersonas] = useState(loadPersonas);\n const [editingPersona, setEditingPersona] = useState<any>(null);\n const [showPersonaEditor, setShowPersonaEditor] = useState(false);\n\n const handlePersonaChange = (id: string) => {\n setPersona(id);\n localStorage.setItem('settings_persona', id);\n const ws = (window as any).__botWs;\n if (ws?.readyState === WebSocket.OPEN) {\n ws.send(JSON.stringify({ type: 'persona_change', persona: id }));\n }\n toast(`Persona: ${personas.find(p => p.id === id)?.name}`, 'success');\n };\n\n const handleSavePersona = () => {\n if (!editingPersona?.name) return;\n const p = { ...editingPersona, id: editingPersona.id || `custom_${Date.now()}` };\n saveCustomPersona(p);\n setPersonas(loadPersonas());\n setShowPersonaEditor(false);\n setEditingPersona(null);\n // Also send to backend to register\n const ws = (window as any).__botWs;\n if (ws?.readyState === WebSocket.OPEN) {\n ws.send(JSON.stringify({ type: 'register_persona', persona: p }));\n }\n toast(`Persona \"${p.name}\" saved`, 'success');\n };\n\n const handleDeletePersona = (id: string) => {\n if (DEFAULT_PERSONAS.find(p => p.id === id)) { toast('Cannot delete built-in persona', 'error'); return; }\n deleteCustomPersona(id);\n setPersonas(loadPersonas());\n if (persona === id) handlePersonaChange('nexus');\n toast('Persona deleted', 'info');\n };\n\n const handleSaveModel = () => {\n localStorage.setItem('settings_model', model);\n if (apiKey) localStorage.setItem('settings_api_key', apiKey);\n else localStorage.removeItem('settings_api_key');\n toast('Model & API key saved', 'success');\n };\n\n const handleLanguage = (lang: string) => {\n setLanguage(lang);\n localStorage.setItem('settings_language', lang);\n toast(`Language: ${lang}`, 'success');\n };\n\n return (\n <div style={{ overflowY: 'auto', padding: '24px 32px', display: 'flex', flexDirection: 'column', gap: 24, height: '100%', maxWidth: 700 }}>\n <div>\n <h1 className=\"font-display\" style={{ fontSize: 44, letterSpacing: 6, color: 'var(--ink)', lineHeight: 1 }}>\n SETTINGS\n </h1>\n <p className=\"font-mono\" style={{ fontSize: 10, color: 'var(--dim)', letterSpacing: 1, marginTop: 4 }}>\n // SYSTEM CONFIGURATION\n </p>\n </div>\n\n {/* LLM Model + API Key — has save button */}\n <SettingSection icon={Cpu} title=\"LLM MODEL\">\n <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>\n {LLM_MODELS.map(m => (\n <label key={m.value} style={{\n display: 'flex', alignItems: 'center', gap: 10, padding: '10px 14px',\n border: `2px solid ${model === m.value ? 'var(--orange)' : 'var(--line)'}`,\n background: model === m.value ? 'rgba(212, 82, 26, 0.06)' : 'transparent',\n cursor: 'pointer', transition: 'all 0.2s',\n }}>\n <input type=\"radio\" name=\"model\" value={m.value} checked={model === m.value}\n onChange={() => setModel(m.value)} style={{ accentColor: 'var(--orange)' }} />\n <div style={{ flex: 1 }}>\n <div style={{ fontSize: 13, fontWeight: 500 }}>{m.label}</div>\n <div className=\"font-mono\" style={{ fontSize: 10, color: 'var(--dim)' }}>{m.desc}</div>\n </div>\n </label>\n ))}\n </div>\n\n <div style={{ marginTop: 14 }}>\n <div className=\"font-mono\" style={{ fontSize: 9, color: 'var(--orange)', letterSpacing: 2, marginBottom: 6, textTransform: 'uppercase', display: 'flex', alignItems: 'center', gap: 6 }}>\n <Shield size={10} /> API Key (optional)\n </div>\n <div style={{ display: 'flex', gap: 8 }}>\n <Input type=\"password\" placeholder=\"sk-... (leave empty for server default)\"\n value={apiKey} onChange={e => setApiKey(e.target.value)} style={{ flex: 1 }} />\n <Button onClick={handleSaveModel} size=\"sm\">SAVE</Button>\n </div>\n </div>\n </SettingSection>\n\n {/* Language — instant save on click */}\n <SettingSection icon={Globe} title=\"LANGUAGE\">\n <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>\n {LANGUAGES.map(l => (\n <button key={l.value} onClick={() => handleLanguage(l.value)} className=\"font-mono\"\n style={{\n padding: '8px 16px', border: `2px solid ${language === l.value ? 'var(--orange)' : 'var(--line)'}`,\n background: language === l.value ? 'rgba(212, 82, 26, 0.06)' : 'var(--cream)',\n color: language === l.value ? 'var(--orange)' : 'var(--ink)',\n fontSize: 12, cursor: 'pointer', transition: 'all 0.2s',\n }}>\n {l.label}\n </button>\n ))}\n </div>\n </SettingSection>\n\n {/* Theme — instant */}\n <SettingSection icon={Palette} title=\"THEME\">\n <div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>\n <button className=\"font-mono\" style={{\n padding: '8px 16px', border: '2px solid var(--orange)',\n background: 'rgba(212, 82, 26, 0.06)', color: 'var(--orange)',\n fontSize: 12, cursor: 'default', textTransform: 'uppercase',\n }}>\n NEXUS WARM\n </button>\n <span className=\"font-mono\" style={{ fontSize: 10, color: 'var(--dim)' }}>\n Dark mode coming soon\n </span>\n </div>\n </SettingSection>\n\n {/* Bot Persona */}\n <SettingSection icon={User} title=\"BOT PERSONA\">\n <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>\n {personas.map(p => (\n <div key={p.id} style={{\n display: 'flex', alignItems: 'center', gap: 10, padding: '10px 14px',\n border: `2px solid ${persona === p.id ? 'var(--orange)' : 'var(--line)'}`,\n background: persona === p.id ? 'rgba(212, 82, 26, 0.06)' : 'transparent',\n cursor: 'pointer', transition: 'all 0.2s',\n }} onClick={() => handlePersonaChange(p.id)}>\n <input type=\"radio\" name=\"persona\" value={p.id} checked={persona === p.id}\n onChange={() => {}} style={{ accentColor: 'var(--orange)' }} />\n <div style={{ flex: 1 }}>\n <div style={{ fontSize: 13, fontWeight: 500 }}>{p.emoji} {p.name}</div>\n <div className=\"font-mono\" style={{ fontSize: 10, color: 'var(--dim)' }}>{p.personality}</div>\n </div>\n <div style={{ display: 'flex', gap: 4 }}>\n <button onClick={e => { e.stopPropagation(); setEditingPersona({ ...p }); setShowPersonaEditor(true); }}\n className=\"font-mono\" style={{ background: 'none', border: '1px solid var(--line)', padding: '2px 6px', fontSize: 9, cursor: 'pointer', color: 'var(--mid)' }}>\n EDIT\n </button>\n {!DEFAULT_PERSONAS.find(d => d.id === p.id) && (\n <button onClick={e => { e.stopPropagation(); handleDeletePersona(p.id); }}\n className=\"font-mono\" style={{ background: 'none', border: '1px solid var(--error)', padding: '2px 6px', fontSize: 9, cursor: 'pointer', color: 'var(--error)' }}>\n DEL\n </button>\n )}\n </div>\n </div>\n ))}\n </div>\n <button onClick={() => { setEditingPersona({ id: '', name: '', emoji: '🤖', personality: '', greeting: '' }); setShowPersonaEditor(true); }}\n className=\"font-mono\" style={{\n marginTop: 10, padding: '8px 14px', border: '2px dashed var(--line)',\n background: 'transparent', color: 'var(--orange)', fontSize: 11,\n cursor: 'pointer', width: '100%', textAlign: 'center',\n }}>\n + CREATE CUSTOM PERSONA\n </button>\n\n {/* Persona Editor */}\n {showPersonaEditor && editingPersona && (\n <div style={{\n marginTop: 12, padding: 14, border: '2px solid var(--orange)',\n background: 'rgba(212, 82, 26, 0.03)', display: 'flex', flexDirection: 'column', gap: 10,\n }}>\n <div className=\"font-display\" style={{ fontSize: 14, letterSpacing: 2 }}>\n {editingPersona.id ? 'EDIT PERSONA' : 'NEW PERSONA'}\n </div>\n <div style={{ display: 'flex', gap: 8 }}>\n <div style={{ flex: 0 }}>\n <div className=\"font-mono\" style={{ fontSize: 9, color: 'var(--dim)', marginBottom: 4 }}>EMOJI</div>\n <Input value={editingPersona.emoji} onChange={e => setEditingPersona({ ...editingPersona, emoji: e.target.value })}\n style={{ width: 50, textAlign: 'center', fontSize: 20 }} />\n </div>\n <div style={{ flex: 1 }}>\n <div className=\"font-mono\" style={{ fontSize: 9, color: 'var(--dim)', marginBottom: 4 }}>NAME</div>\n <Input value={editingPersona.name} onChange={e => setEditingPersona({ ...editingPersona, name: e.target.value })}\n placeholder=\"e.g. 蟹老板\" />\n </div>\n </div>\n <div>\n <div className=\"font-mono\" style={{ fontSize: 9, color: 'var(--dim)', marginBottom: 4 }}>PERSONALITY</div>\n <Input value={editingPersona.personality} onChange={e => setEditingPersona({ ...editingPersona, personality: e.target.value })}\n placeholder=\"e.g. Cheerful, humorous, speaks with crab puns\" />\n </div>\n <div>\n <div className=\"font-mono\" style={{ fontSize: 9, color: 'var(--dim)', marginBottom: 4 }}>GREETING</div>\n <Input value={editingPersona.greeting} onChange={e => setEditingPersona({ ...editingPersona, greeting: e.target.value })}\n placeholder=\"e.g. 嘿!我是蟹老板 🦀\" />\n </div>\n <div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end' }}>\n <Button variant=\"outline\" size=\"sm\" onClick={() => { setShowPersonaEditor(false); setEditingPersona(null); }}>CANCEL</Button>\n <Button size=\"sm\" onClick={handleSavePersona}>SAVE</Button>\n </div>\n </div>\n )}\n </SettingSection>\n\n {/* Bot — instant toggle + slider */}\n <SettingSection icon={Bot} title=\"3D BOT\">\n <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>\n <label style={{ display: 'flex', alignItems: 'center', gap: 8, cursor: 'pointer' }}>\n <input type=\"checkbox\" checked={botEnabled} onChange={e => setBotEnabled(e.target.checked)}\n style={{ width: 18, height: 18, accentColor: 'var(--orange)' }} />\n <span style={{ fontSize: 13 }}>{botEnabled ? 'Enabled' : 'Disabled'}</span>\n </label>\n {botEnabled && (\n <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>\n <span className=\"font-mono\" style={{ fontSize: 10, color: 'var(--dim)', minWidth: 30 }}>Size</span>\n <input type=\"range\" min={80} max={260} value={botSize}\n onChange={e => setBotSize(Number(e.target.value))}\n style={{ flex: 1, maxWidth: 200, accentColor: 'var(--orange)' }} />\n <span className=\"font-mono\" style={{ fontSize: 10, color: 'var(--dim)', minWidth: 40 }}>{botSize}px</span>\n </div>\n )}\n </div>\n </SettingSection>\n </div>\n );\n}\n\nfunction SettingSection({ icon: Icon, title, children }: {\n icon: any; title: string; children: React.ReactNode;\n}) {\n return (\n <div className=\"animate-card-in\" style={{ border: '2px solid var(--ink)', padding: '18px 20px', background: 'var(--cream)' }}>\n <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 14 }}>\n <Icon size={16} color=\"var(--orange)\" />\n <span className=\"font-display\" style={{ fontSize: 16, letterSpacing: 3 }}>{title}</span>\n </div>\n {children}\n </div>\n );\n}\n"}, "e5e9dea2a579cb9d20228420733ad97273d94cdf": {"t": 1785749335.2502935, "n": 9222, "h": "367ffd05a0861375", "c": "import { useState } from 'react';\nimport { useBotStore } from '../../store/bot';\nimport { useThemeStore } from '../../store/theme';\nimport { Button } from '../../components/ui/Button';\nimport { Input } from '../../components/ui/Input';\nimport { toast } from '../../components/ui/Toast';\nimport { Bot, Cpu, Palette, Globe, Shield, User } from 'lucide-react';\n\nconst LLM_MODELS = [\n { value: 'deepseek/deepseek-chat', label: 'DeepSeek Chat', desc: 'Fast & cheap' },\n { value: 'openai/gpt-4o', label: 'GPT-4o', desc: 'Popular' },\n { value: 'anthropic/claude-sonnet-4-20250514', label: 'Claude Sonnet', desc: 'Best quality' },\n { value: 'ollama/llama3', label: 'Ollama Llama3', desc: 'Free, local' },\n { value: 'ollama/qwen2.5', label: 'Ollama Qwen2.5', desc: 'Free, local, Chinese' },\n];\n\nconst LANGUAGES = [\n { value: 'auto', label: 'Auto (match user)' },\n { value: 'en', label: 'English' },\n { value: 'zh', label: '中文' },\n { value: 'ja', label: '日本語' },\n];\n\nconst PERSONAS = [\n { id: 'crab_boss', label: '蟹老板', emoji: '🦀', desc: 'Cheerful, humorous, business-minded crab' },\n { id: 'nexus', label: 'Nexus', emoji: '🤖', desc: 'Professional, concise, friendly assistant' },\n { id: 'buddy', label: 'Buddy', emoji: '🎉', desc: 'Casual, talkative, lots of emoji' },\n];\n\nexport default function SettingsPage() {\n const { enabled: botEnabled, setEnabled: setBotEnabled, size: botSize, setSize: setBotSize } = useBotStore();\n const { theme } = useThemeStore();\n\n const [model, setModel] = useState(() => localStorage.getItem('settings_model') || 'deepseek/deepseek-chat');\n const [apiKey, setApiKey] = useState(() => localStorage.getItem('settings_api_key') || '');\n const [language, setLanguage] = useState(() => localStorage.getItem('settings_language') || 'auto');\n const [persona, setPersona] = useState(() => localStorage.getItem('settings_persona') || 'crab_boss');\n\n const handlePersonaChange = (id: string) => {\n setPersona(id);\n localStorage.setItem('settings_persona', id);\n // Send persona change via WebSocket\n const ws = (window as any).__botWs;\n if (ws?.readyState === WebSocket.OPEN) {\n ws.send(JSON.stringify({ type: 'persona_change', persona: id }));\n }\n toast(`Persona: ${PERSONAS.find(p => p.id === id)?.label}`, 'success');\n };\n\n const handleSaveModel = () => {\n localStorage.setItem('settings_model', model);\n if (apiKey) localStorage.setItem('settings_api_key', apiKey);\n else localStorage.removeItem('settings_api_key');\n toast('Model & API key saved', 'success');\n };\n\n const handleLanguage = (lang: string) => {\n setLanguage(lang);\n localStorage.setItem('settings_language', lang);\n toast(`Language: ${lang}`, 'success');\n };\n\n return (\n <div style={{ overflowY: 'auto', padding: '24px 32px', display: 'flex', flexDirection: 'column', gap: 24, height: '100%', maxWidth: 700 }}>\n <div>\n <h1 className=\"font-display\" style={{ fontSize: 44, letterSpacing: 6, color: 'var(--ink)', lineHeight: 1 }}>\n SETTINGS\n </h1>\n <p className=\"font-mono\" style={{ fontSize: 10, color: 'var(--dim)', letterSpacing: 1, marginTop: 4 }}>\n // SYSTEM CONFIGURATION\n </p>\n </div>\n\n {/* LLM Model + API Key — has save button */}\n <SettingSection icon={Cpu} title=\"LLM MODEL\">\n <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>\n {LLM_MODELS.map(m => (\n <label key={m.value} style={{\n display: 'flex', alignItems: 'center', gap: 10, padding: '10px 14px',\n border: `2px solid ${model === m.value ? 'var(--orange)' : 'var(--line)'}`,\n background: model === m.value ? 'rgba(212, 82, 26, 0.06)' : 'transparent',\n cursor: 'pointer', transition: 'all 0.2s',\n }}>\n <input type=\"radio\" name=\"model\" value={m.value} checked={model === m.value}\n onChange={() => setModel(m.value)} style={{ accentColor: 'var(--orange)' }} />\n <div style={{ flex: 1 }}>\n <div style={{ fontSize: 13, fontWeight: 500 }}>{m.label}</div>\n <div className=\"font-mono\" style={{ fontSize: 10, color: 'var(--dim)' }}>{m.desc}</div>\n </div>\n </label>\n ))}\n </div>\n\n <div style={{ marginTop: 14 }}>\n <div className=\"font-mono\" style={{ fontSize: 9, color: 'var(--orange)', letterSpacing: 2, marginBottom: 6, textTransform: 'uppercase', display: 'flex', alignItems: 'center', gap: 6 }}>\n <Shield size={10} /> API Key (optional)\n </div>\n <div style={{ display: 'flex', gap: 8 }}>\n <Input type=\"password\" placeholder=\"sk-... (leave empty for server default)\"\n value={apiKey} onChange={e => setApiKey(e.target.value)} style={{ flex: 1 }} />\n <Button onClick={handleSaveModel} size=\"sm\">SAVE</Button>\n </div>\n </div>\n </SettingSection>\n\n {/* Language — instant save on click */}\n <SettingSection icon={Globe} title=\"LANGUAGE\">\n <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>\n {LANGUAGES.map(l => (\n <button key={l.value} onClick={() => handleLanguage(l.value)} className=\"font-mono\"\n style={{\n padding: '8px 16px', border: `2px solid ${language === l.value ? 'var(--orange)' : 'var(--line)'}`,\n background: language === l.value ? 'rgba(212, 82, 26, 0.06)' : 'var(--cream)',\n color: language === l.value ? 'var(--orange)' : 'var(--ink)',\n fontSize: 12, cursor: 'pointer', transition: 'all 0.2s',\n }}>\n {l.label}\n </button>\n ))}\n </div>\n </SettingSection>\n\n {/* Theme — instant */}\n <SettingSection icon={Palette} title=\"THEME\">\n <div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>\n <button className=\"font-mono\" style={{\n padding: '8px 16px', border: '2px solid var(--orange)',\n background: 'rgba(212, 82, 26, 0.06)', color: 'var(--orange)',\n fontSize: 12, cursor: 'default', textTransform: 'uppercase',\n }}>\n NEXUS WARM\n </button>\n <span className=\"font-mono\" style={{ fontSize: 10, color: 'var(--dim)' }}>\n Dark mode coming soon\n </span>\n </div>\n </SettingSection>\n\n {/* Bot Persona */}\n <SettingSection icon={User} title=\"BOT PERSONA\">\n <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>\n {PERSONAS.map(p => (\n <label key={p.id} style={{\n display: 'flex', alignItems: 'center', gap: 10, padding: '10px 14px',\n border: `2px solid ${persona === p.id ? 'var(--orange)' : 'var(--line)'}`,\n background: persona === p.id ? 'rgba(212, 82, 26, 0.06)' : 'transparent',\n cursor: 'pointer', transition: 'all 0.2s',\n }}>\n <input type=\"radio\" name=\"persona\" value={p.id} checked={persona === p.id}\n onChange={() => handlePersonaChange(p.id)} style={{ accentColor: 'var(--orange)' }} />\n <div style={{ flex: 1 }}>\n <div style={{ fontSize: 13, fontWeight: 500 }}>{p.emoji} {p.label}</div>\n <div className=\"font-mono\" style={{ fontSize: 10, color: 'var(--dim)' }}>{p.desc}</div>\n </div>\n </label>\n ))}\n </div>\n </SettingSection>\n\n {/* Bot — instant toggle + slider */}\n <SettingSection icon={Bot} title=\"3D BOT\">\n <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>\n <label style={{ display: 'flex', alignItems: 'center', gap: 8, cursor: 'pointer' }}>\n <input type=\"checkbox\" checked={botEnabled} onChange={e => setBotEnabled(e.target.checked)}\n style={{ width: 18, height: 18, accentColor: 'var(--orange)' }} />\n <span style={{ fontSize: 13 }}>{botEnabled ? 'Enabled' : 'Disabled'}</span>\n </label>\n {botEnabled && (\n <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>\n <span className=\"font-mono\" style={{ fontSize: 10, color: 'var(--dim)', minWidth: 30 }}>Size</span>\n <input type=\"range\" min={80} max={260} value={botSize}\n onChange={e => setBotSize(Number(e.target.value))}\n style={{ flex: 1, maxWidth: 200, accentColor: 'var(--orange)' }} />\n <span className=\"font-mono\" style={{ fontSize: 10, color: 'var(--dim)', minWidth: 40 }}>{botSize}px</span>\n </div>\n )}\n </div>\n </SettingSection>\n </div>\n );\n}\n\nfunction SettingSection({ icon: Icon, title, children }: {\n icon: any; title: string; children: React.ReactNode;\n}) {\n return (\n <div className=\"animate-card-in\" style={{ border: '2px solid var(--ink)', padding: '18px 20px', background: 'var(--cream)' }}>\n <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 14 }}>\n <Icon size={16} color=\"var(--orange)\" />\n <span className=\"font-display\" style={{ fontSize: 16, letterSpacing: 3 }}>{title}</span>\n </div>\n {children}\n </div>\n );\n}\n"}, "61ee7da4974223dd3d8921895a16da3d2e866602": {"t": 1785749336.8376625, "n": 7317, "h": "c3829a7288a40328", "c": "import { useState } from 'react';\nimport { useBotStore } from '../../store/bot';\nimport { useThemeStore } from '../../store/theme';\nimport { Button } from '../../components/ui/Button';\nimport { Input } from '../../components/ui/Input';\nimport { toast } from '../../components/ui/Toast';\nimport { Bot, Cpu, Palette, Globe, Shield } from 'lucide-react';\n\nconst LLM_MODELS = [\n { value: 'deepseek/deepseek-chat', label: 'DeepSeek Chat', desc: 'Fast & cheap' },\n { value: 'openai/gpt-4o', label: 'GPT-4o', desc: 'Popular' },\n { value: 'anthropic/claude-sonnet-4-20250514', label: 'Claude Sonnet', desc: 'Best quality' },\n { value: 'ollama/llama3', label: 'Ollama Llama3', desc: 'Free, local' },\n { value: 'ollama/qwen2.5', label: 'Ollama Qwen2.5', desc: 'Free, local, Chinese' },\n];\n\nconst LANGUAGES = [\n { value: 'auto', label: 'Auto (match user)' },\n { value: 'en', label: 'English' },\n { value: 'zh', label: '中文' },\n { value: 'ja', label: '日本語' },\n];\n\nexport default function SettingsPage() {\n const { enabled: botEnabled, setEnabled: setBotEnabled, size: botSize, setSize: setBotSize } = useBotStore();\n const { theme } = useThemeStore();\n\n const [model, setModel] = useState(() => localStorage.getItem('settings_model') || 'deepseek/deepseek-chat');\n const [apiKey, setApiKey] = useState(() => localStorage.getItem('settings_api_key') || '');\n const [language, setLanguage] = useState(() => localStorage.getItem('settings_language') || 'auto');\n\n const handleSaveModel = () => {\n localStorage.setItem('settings_model', model);\n if (apiKey) localStorage.setItem('settings_api_key', apiKey);\n else localStorage.removeItem('settings_api_key');\n toast('Model & API key saved', 'success');\n };\n\n const handleLanguage = (lang: string) => {\n setLanguage(lang);\n localStorage.setItem('settings_language', lang);\n toast(`Language: ${lang}`, 'success');\n };\n\n return (\n <div style={{ overflowY: 'auto', padding: '24px 32px', display: 'flex', flexDirection: 'column', gap: 24, height: '100%', maxWidth: 700 }}>\n <div>\n <h1 className=\"font-display\" style={{ fontSize: 44, letterSpacing: 6, color: 'var(--ink)', lineHeight: 1 }}>\n SETTINGS\n </h1>\n <p className=\"font-mono\" style={{ fontSize: 10, color: 'var(--dim)', letterSpacing: 1, marginTop: 4 }}>\n // SYSTEM CONFIGURATION\n </p>\n </div>\n\n {/* LLM Model + API Key — has save button */}\n <SettingSection icon={Cpu} title=\"LLM MODEL\">\n <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>\n {LLM_MODELS.map(m => (\n <label key={m.value} style={{\n display: 'flex', alignItems: 'center', gap: 10, padding: '10px 14px',\n border: `2px solid ${model === m.value ? 'var(--orange)' : 'var(--line)'}`,\n background: model === m.value ? 'rgba(212, 82, 26, 0.06)' : 'transparent',\n cursor: 'pointer', transition: 'all 0.2s',\n }}>\n <input type=\"radio\" name=\"model\" value={m.value} checked={model === m.value}\n onChange={() => setModel(m.value)} style={{ accentColor: 'var(--orange)' }} />\n <div style={{ flex: 1 }}>\n <div style={{ fontSize: 13, fontWeight: 500 }}>{m.label}</div>\n <div className=\"font-mono\" style={{ fontSize: 10, color: 'var(--dim)' }}>{m.desc}</div>\n </div>\n </label>\n ))}\n </div>\n\n <div style={{ marginTop: 14 }}>\n <div className=\"font-mono\" style={{ fontSize: 9, color: 'var(--orange)', letterSpacing: 2, marginBottom: 6, textTransform: 'uppercase', display: 'flex', alignItems: 'center', gap: 6 }}>\n <Shield size={10} /> API Key (optional)\n </div>\n <div style={{ display: 'flex', gap: 8 }}>\n <Input type=\"password\" placeholder=\"sk-... (leave empty for server default)\"\n value={apiKey} onChange={e => setApiKey(e.target.value)} style={{ flex: 1 }} />\n <Button onClick={handleSaveModel} size=\"sm\">SAVE</Button>\n </div>\n </div>\n </SettingSection>\n\n {/* Language — instant save on click */}\n <SettingSection icon={Globe} title=\"LANGUAGE\">\n <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>\n {LANGUAGES.map(l => (\n <button key={l.value} onClick={() => handleLanguage(l.value)} className=\"font-mono\"\n style={{\n padding: '8px 16px', border: `2px solid ${language === l.value ? 'var(--orange)' : 'var(--line)'}`,\n background: language === l.value ? 'rgba(212, 82, 26, 0.06)' : 'var(--cream)',\n color: language === l.value ? 'var(--orange)' : 'var(--ink)',\n fontSize: 12, cursor: 'pointer', transition: 'all 0.2s',\n }}>\n {l.label}\n </button>\n ))}\n </div>\n </SettingSection>\n\n {/* Theme — instant */}\n <SettingSection icon={Palette} title=\"THEME\">\n <div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>\n <button className=\"font-mono\" style={{\n padding: '8px 16px', border: '2px solid var(--orange)',\n background: 'rgba(212, 82, 26, 0.06)', color: 'var(--orange)',\n fontSize: 12, cursor: 'default', textTransform: 'uppercase',\n }}>\n NEXUS WARM\n </button>\n <span className=\"font-mono\" style={{ fontSize: 10, color: 'var(--dim)' }}>\n Dark mode coming soon\n </span>\n </div>\n </SettingSection>\n\n {/* Bot — instant toggle + slider */}\n <SettingSection icon={Bot} title=\"3D BOT\">\n <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>\n <label style={{ display: 'flex', alignItems: 'center', gap: 8, cursor: 'pointer' }}>\n <input type=\"checkbox\" checked={botEnabled} onChange={e => setBotEnabled(e.target.checked)}\n style={{ width: 18, height: 18, accentColor: 'var(--orange)' }} />\n <span style={{ fontSize: 13 }}>{botEnabled ? 'Enabled' : 'Disabled'}</span>\n </label>\n {botEnabled && (\n <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>\n <span className=\"font-mono\" style={{ fontSize: 10, color: 'var(--dim)', minWidth: 30 }}>Size</span>\n <input type=\"range\" min={80} max={260} value={botSize}\n onChange={e => setBotSize(Number(e.target.value))}\n style={{ flex: 1, maxWidth: 200, accentColor: 'var(--orange)' }} />\n <span className=\"font-mono\" style={{ fontSize: 10, color: 'var(--dim)', minWidth: 40 }}>{botSize}px</span>\n </div>\n )}\n </div>\n </SettingSection>\n </div>\n );\n}\n\nfunction SettingSection({ icon: Icon, title, children }: {\n icon: any; title: string; children: React.ReactNode;\n}) {\n return (\n <div className=\"animate-card-in\" style={{ border: '2px solid var(--ink)', padding: '18px 20px', background: 'var(--cream)' }}>\n <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 14 }}>\n <Icon size={16} color=\"var(--orange)\" />\n <span className=\"font-display\" style={{ fontSize: 16, letterSpacing: 3 }}>{title}</span>\n </div>\n {children}\n </div>\n );\n}\n"}}}} |