refactor: enhance setup page with translation support and default configurations
- Added translation support for various text elements using `useTranslation`. - Created default personality and emoji configurations to streamline setup. - Updated step titles and descriptions to use translated strings. - Improved validation messages to be translatable. - Refactored loading and success/error messages for better user feedback. - Enhanced UI structure for better readability and maintainability.
This commit is contained in:
@@ -379,6 +379,220 @@
|
||||
"switchToLight": "Switch to light mode",
|
||||
"switchToDark": "Switch to dark mode"
|
||||
},
|
||||
"setupPage": {
|
||||
"steps": {
|
||||
"botBasic": {
|
||||
"title": "Bot Basics",
|
||||
"description": "Configure the bot's basic information"
|
||||
},
|
||||
"personality": {
|
||||
"title": "Personality",
|
||||
"description": "Define the bot's personality and speaking style"
|
||||
},
|
||||
"emoji": {
|
||||
"title": "Emoji",
|
||||
"description": "Configure emoji-related settings"
|
||||
},
|
||||
"other": {
|
||||
"title": "Other Settings",
|
||||
"description": "Configure tools, emotion system, and more"
|
||||
},
|
||||
"siliconFlow": {
|
||||
"title": "API Setup",
|
||||
"description": "Configure the SiliconFlow API key"
|
||||
}
|
||||
},
|
||||
"loading": {
|
||||
"title": "Loading configuration...",
|
||||
"description": "Reading existing configuration"
|
||||
},
|
||||
"header": {
|
||||
"title": "Initial Setup Wizard",
|
||||
"description": "Let's complete the initial setup for {{appName}}"
|
||||
},
|
||||
"progress": {
|
||||
"stepCounter": "Step {{current}} / {{total}}"
|
||||
},
|
||||
"validation": {
|
||||
"selectPlatform": "Please select a platform",
|
||||
"enterNickname": "Please enter a nickname",
|
||||
"enterQqAccount": "Please enter a QQ account",
|
||||
"enterAccountId": "Please enter an account ID"
|
||||
},
|
||||
"toast": {
|
||||
"loadFailedTitle": "Failed to load configuration",
|
||||
"loadFailedDescription": "Unable to load the existing configuration. Default values will be used.",
|
||||
"saveSuccessTitle": "Saved successfully",
|
||||
"saveSuccessDescription": "{{step}} configuration has been saved",
|
||||
"saveFailedTitle": "Save failed",
|
||||
"validationFailedTitle": "Validation failed",
|
||||
"completeSuccessTitle": "Setup complete",
|
||||
"completeSuccessDescription": "{{appName}} is restarting to apply the new configuration...",
|
||||
"completeFailedTitle": "Setup failed",
|
||||
"skipFailedTitle": "Failed to skip the setup wizard",
|
||||
"unknownError": "Unknown error"
|
||||
},
|
||||
"actions": {
|
||||
"previous": "Previous",
|
||||
"next": "Next",
|
||||
"skip": "Skip Wizard",
|
||||
"complete": "Complete Setup",
|
||||
"saving": "Saving...",
|
||||
"completing": "Completing..."
|
||||
},
|
||||
"skipDialog": {
|
||||
"title": "Skip the setup wizard?",
|
||||
"description": "You can reopen the setup wizard anytime from System Settings. Are you sure you want to skip it?",
|
||||
"confirm": "Skip Anyway"
|
||||
},
|
||||
"footer": "You can change these settings anytime in Settings",
|
||||
"defaults": {
|
||||
"personality": {
|
||||
"personality": "She is a sophomore college student who spends time on Tieba.",
|
||||
"replyStyle": "Keep replies plain and concise, speak Chinese, and avoid deliberately emphasizing any academic background. You can refer to the reply styles commonly seen on Tieba, Zhihu, and Weibo.",
|
||||
"interest": "Interested in technology, games, anime, and everyday topics, and dislikes topics that are overly heavy or serious.",
|
||||
"planStyle": "1. Consider **every action** in **all** available actions and use it if its conditions match the current conversation.\n2. Do not repeat content that has already been executed.\n3. Control how often you speak and avoid replying too frequently.\n4. If someone seems annoyed with you, reduce your replies.\n5. If someone attacks you or becomes emotional, respond appropriately.",
|
||||
"privatePlanStyle": "1. Consider **every action** in **all** available actions and use it if its conditions match the current conversation.\n2. Do not repeat content that has already been executed.\n3. If a sentence has already been replied to, do not reply to it again."
|
||||
},
|
||||
"emoji": {
|
||||
"filtrationPrompt": "Appropriate and safe for general audiences"
|
||||
}
|
||||
},
|
||||
"forms": {
|
||||
"botBasic": {
|
||||
"platform": {
|
||||
"label": "Platform *",
|
||||
"placeholder": "Select a platform",
|
||||
"description": "Choose the platform where the bot runs",
|
||||
"options": {
|
||||
"custom": "Other Platform"
|
||||
}
|
||||
},
|
||||
"customPlatform": {
|
||||
"label": "Platform Name *",
|
||||
"placeholder": "Enter a platform name, such as matrix"
|
||||
},
|
||||
"qqAccount": {
|
||||
"label": "QQ Account *",
|
||||
"placeholder": "Enter the bot's QQ account",
|
||||
"description": "The QQ account used to log in the bot"
|
||||
},
|
||||
"primaryAccount": {
|
||||
"label": "Account ID *",
|
||||
"placeholder": "Enter the bot's account ID",
|
||||
"description": "The bot's account identifier on this platform"
|
||||
},
|
||||
"nickname": {
|
||||
"label": "Nickname *",
|
||||
"placeholder": "Enter the bot's nickname",
|
||||
"description": "The bot's primary display name"
|
||||
},
|
||||
"alias": {
|
||||
"label": "Aliases",
|
||||
"placeholder": "Type an alias and press Enter to add it",
|
||||
"add": "Add",
|
||||
"description": "Other names for the bot. You can add multiple aliases.",
|
||||
"remove": "Remove alias {{alias}}"
|
||||
}
|
||||
},
|
||||
"personality": {
|
||||
"personality": {
|
||||
"label": "Personality Traits *",
|
||||
"placeholder": "Describe the bot's personality and identity traits (recommended within 120 characters)",
|
||||
"description": "Example: a sophomore college girl who browses Tieba"
|
||||
},
|
||||
"replyStyle": {
|
||||
"label": "Reply Style *",
|
||||
"placeholder": "Describe how the bot speaks and expresses itself",
|
||||
"description": "Example: keep replies plain and concise, speak Chinese, and refer to styles seen on Tieba, Zhihu, and Weibo"
|
||||
},
|
||||
"interest": {
|
||||
"label": "Interests *",
|
||||
"placeholder": "Describe the topics the bot is interested in",
|
||||
"description": "This affects which topics the bot is more likely to respond to"
|
||||
},
|
||||
"planStyle": {
|
||||
"label": "Group Chat Rules *",
|
||||
"placeholder": "The bot's behavior style and rules in group chats",
|
||||
"description": "Defines how the bot acts in group chats, such as reply frequency and conditions"
|
||||
},
|
||||
"privatePlanStyle": {
|
||||
"label": "Private Chat Rules *",
|
||||
"placeholder": "The bot's behavior style and rules in private chats",
|
||||
"description": "Defines how the bot behaves in private chats"
|
||||
}
|
||||
},
|
||||
"emoji": {
|
||||
"emojiChance": {
|
||||
"label": "Emoji Activation Chance",
|
||||
"description": "How likely the bot is to send an emoji"
|
||||
},
|
||||
"maxRegNum": {
|
||||
"label": "Maximum Emoji Count",
|
||||
"description": "The maximum number of emojis the bot can store"
|
||||
},
|
||||
"doReplace": {
|
||||
"label": "Replace When Full",
|
||||
"description": "When enabled, old emojis will be removed; when disabled, new emojis will no longer be collected"
|
||||
},
|
||||
"checkInterval": {
|
||||
"label": "Check Interval (Minutes)",
|
||||
"description": "How often to check emoji registration, damage, and deletion"
|
||||
},
|
||||
"stealEmoji": {
|
||||
"label": "Collect Emojis",
|
||||
"description": "Allow the bot to keep certain emojis for itself"
|
||||
},
|
||||
"contentFiltration": {
|
||||
"label": "Enable Emoji Filtering",
|
||||
"description": "Only save emojis that meet the requirements"
|
||||
},
|
||||
"filtrationPrompt": {
|
||||
"label": "Filter Requirements",
|
||||
"placeholder": "For example: appropriate and safe for general audiences",
|
||||
"description": "Describe what kind of emojis should be allowed"
|
||||
}
|
||||
},
|
||||
"other": {
|
||||
"enableTool": {
|
||||
"label": "Enable Tool System",
|
||||
"description": "Allow the bot to use tools to enhance its capabilities"
|
||||
},
|
||||
"allGlobal": {
|
||||
"label": "Enable Global Slang Mode",
|
||||
"description": "Allow the bot to learn and use group-specific slang"
|
||||
}
|
||||
},
|
||||
"siliconFlow": {
|
||||
"about": {
|
||||
"title": "About SiliconFlow",
|
||||
"description": "SiliconFlow provides broad model coverage, including DeepSeek V3, Qwen, vision models, speech recognition, and embedding models. A single API key unlocks all MaiBot features.",
|
||||
"link": "Get an API key from SiliconFlow"
|
||||
},
|
||||
"apiKey": {
|
||||
"label": "SiliconFlow API Key *",
|
||||
"description": "Enter your SiliconFlow API key. Once provided, MaiBot will automatically configure all required models.",
|
||||
"show": "Show API key",
|
||||
"hide": "Hide API key"
|
||||
},
|
||||
"autoConfig": {
|
||||
"title": "The following models will be configured automatically:",
|
||||
"items": {
|
||||
"deepseek": "DeepSeek V3 - primary chat and tool model",
|
||||
"qwen3": "Qwen3 30B - frequent small tasks and tool calls",
|
||||
"qwen3Vl": "Qwen3 VL 30B - image recognition",
|
||||
"senseVoice": "SenseVoice - speech recognition",
|
||||
"bgeM3": "BGE-M3 - text embeddings",
|
||||
"lpmm": "Knowledge-base-related models (LPMM)"
|
||||
}
|
||||
},
|
||||
"hint": {
|
||||
"title": "Tip: ",
|
||||
"description": "After finishing the wizard, you can add more API providers and models in \"System Settings -> Model Config\"."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"loading": "Loading...",
|
||||
"error": "Error",
|
||||
|
||||
@@ -379,6 +379,220 @@
|
||||
"switchToLight": "ライトモードに切り替える",
|
||||
"switchToDark": "ダークモードに切り替える"
|
||||
},
|
||||
"setupPage": {
|
||||
"steps": {
|
||||
"botBasic": {
|
||||
"title": "Bot基本設定",
|
||||
"description": "ボットの基本情報を設定します"
|
||||
},
|
||||
"personality": {
|
||||
"title": "人格設定",
|
||||
"description": "ボットの性格や話し方を定義します"
|
||||
},
|
||||
"emoji": {
|
||||
"title": "絵文字パック",
|
||||
"description": "絵文字パック関連の設定を行います"
|
||||
},
|
||||
"other": {
|
||||
"title": "その他の設定",
|
||||
"description": "ツールや感情システムなどを設定します"
|
||||
},
|
||||
"siliconFlow": {
|
||||
"title": "API設定",
|
||||
"description": "SiliconFlow API キーを設定します"
|
||||
}
|
||||
},
|
||||
"loading": {
|
||||
"title": "設定を読み込み中...",
|
||||
"description": "既存の設定を読み取っています"
|
||||
},
|
||||
"header": {
|
||||
"title": "初回セットアップウィザード",
|
||||
"description": "{{appName}} の初期設定を一緒に完了しましょう"
|
||||
},
|
||||
"progress": {
|
||||
"stepCounter": "ステップ {{current}} / {{total}}"
|
||||
},
|
||||
"validation": {
|
||||
"selectPlatform": "プラットフォームを選択してください",
|
||||
"enterNickname": "ニックネームを入力してください",
|
||||
"enterQqAccount": "QQ アカウントを入力してください",
|
||||
"enterAccountId": "アカウント ID を入力してください"
|
||||
},
|
||||
"toast": {
|
||||
"loadFailedTitle": "設定の読み込みに失敗しました",
|
||||
"loadFailedDescription": "既存の設定を読み込めなかったため、デフォルト値を使用します",
|
||||
"saveSuccessTitle": "保存しました",
|
||||
"saveSuccessDescription": "{{step}} を保存しました",
|
||||
"saveFailedTitle": "保存に失敗しました",
|
||||
"validationFailedTitle": "入力内容を確認してください",
|
||||
"completeSuccessTitle": "設定が完了しました",
|
||||
"completeSuccessDescription": "新しい設定を反映するために {{appName}} を再起動しています...",
|
||||
"completeFailedTitle": "設定の完了に失敗しました",
|
||||
"skipFailedTitle": "セットアップのスキップに失敗しました",
|
||||
"unknownError": "不明なエラー"
|
||||
},
|
||||
"actions": {
|
||||
"previous": "前へ",
|
||||
"next": "次へ",
|
||||
"skip": "ウィザードをスキップ",
|
||||
"complete": "設定を完了",
|
||||
"saving": "保存中...",
|
||||
"completing": "完了処理中..."
|
||||
},
|
||||
"skipDialog": {
|
||||
"title": "セットアップウィザードをスキップしますか?",
|
||||
"description": "システム設定からいつでも再度セットアップウィザードを開けます。スキップしてもよろしいですか?",
|
||||
"confirm": "スキップする"
|
||||
},
|
||||
"footer": "これらの設定はいつでも設定画面から変更できます",
|
||||
"defaults": {
|
||||
"personality": {
|
||||
"personality": "女子大生で、現在大学2年生。掲示板を見るのが好き。",
|
||||
"replyStyle": "返信は淡々と、短めにし、中国語で話してください。自分の学科背景をわざと強調しないでください。Tieba、Zhihu、Weibo の返信スタイルを参考にできます。",
|
||||
"interest": "技術、ゲーム、アニメ、日常の話題に興味があり、重すぎたり厳粛すぎたりする話題は好みません。",
|
||||
"planStyle": "1. 利用可能な **すべて** の action の **各アクション** が現在の条件に合うか考え、会話内容に合えば使用してください\n2. 同じ内容がすでに実行されている場合は繰り返さないでください\n3. 発言頻度を調整し、発言しすぎないでください\n4. 誰かがあなたにうんざりしている場合は、返信を減らしてください\n5. 誰かがあなたを攻撃したり感情的になったりした場合は、適切に対応してください",
|
||||
"privatePlanStyle": "1. 利用可能な **すべて** の action の **各アクション** が現在の条件に合うか考え、会話内容に合えば使用してください\n2. 同じ内容がすでに実行されている場合は繰り返さないでください\n3. すでに返信した文には再度返信しないでください"
|
||||
},
|
||||
"emoji": {
|
||||
"filtrationPrompt": "公序良俗に反しないこと"
|
||||
}
|
||||
},
|
||||
"forms": {
|
||||
"botBasic": {
|
||||
"platform": {
|
||||
"label": "プラットフォーム *",
|
||||
"placeholder": "プラットフォームを選択",
|
||||
"description": "ボットが動作するプラットフォームを選択します",
|
||||
"options": {
|
||||
"custom": "その他のプラットフォーム"
|
||||
}
|
||||
},
|
||||
"customPlatform": {
|
||||
"label": "プラットフォーム名 *",
|
||||
"placeholder": "matrix などのプラットフォーム名を入力"
|
||||
},
|
||||
"qqAccount": {
|
||||
"label": "QQ アカウント *",
|
||||
"placeholder": "ボットの QQ アカウントを入力",
|
||||
"description": "ボットのログインに使用する QQ アカウントです"
|
||||
},
|
||||
"primaryAccount": {
|
||||
"label": "アカウント ID *",
|
||||
"placeholder": "ボットのアカウント ID を入力",
|
||||
"description": "このプラットフォーム上でのボットのアカウント識別子です"
|
||||
},
|
||||
"nickname": {
|
||||
"label": "ニックネーム *",
|
||||
"placeholder": "ボットのニックネームを入力",
|
||||
"description": "ボットの主な呼び名です"
|
||||
},
|
||||
"alias": {
|
||||
"label": "別名",
|
||||
"placeholder": "別名を入力して Enter で追加",
|
||||
"add": "追加",
|
||||
"description": "ボットの他の呼び名を複数追加できます",
|
||||
"remove": "別名 {{alias}} を削除"
|
||||
}
|
||||
},
|
||||
"personality": {
|
||||
"personality": {
|
||||
"label": "人格特性 *",
|
||||
"placeholder": "ボットの人格や設定を説明してください(120文字以内推奨)",
|
||||
"description": "例:大学2年生の女子大生で、Tieba をよく見ている"
|
||||
},
|
||||
"replyStyle": {
|
||||
"label": "話し方 *",
|
||||
"placeholder": "ボットの話し方や表現の癖を説明してください",
|
||||
"description": "例:返信は淡々と短めにし、中国語で話し、Tieba・Zhihu・Weibo の雰囲気を参考にする"
|
||||
},
|
||||
"interest": {
|
||||
"label": "興味 *",
|
||||
"placeholder": "ボットが興味を持つ話題を説明してください",
|
||||
"description": "どの話題に返信しやすくなるかに影響します"
|
||||
},
|
||||
"planStyle": {
|
||||
"label": "グループチャットのルール *",
|
||||
"placeholder": "グループチャットでの行動方針やルール",
|
||||
"description": "返信頻度や条件など、グループチャットでの振る舞いを定義します"
|
||||
},
|
||||
"privatePlanStyle": {
|
||||
"label": "個別チャットのルール *",
|
||||
"placeholder": "個別チャットでの行動方針やルール",
|
||||
"description": "個別チャットでの振る舞いを定義します"
|
||||
}
|
||||
},
|
||||
"emoji": {
|
||||
"emojiChance": {
|
||||
"label": "絵文字パック発動確率",
|
||||
"description": "ボットが絵文字を送る確率です"
|
||||
},
|
||||
"maxRegNum": {
|
||||
"label": "最大絵文字数",
|
||||
"description": "ボットが保存できる絵文字の最大数です"
|
||||
},
|
||||
"doReplace": {
|
||||
"label": "上限到達時に置き換える",
|
||||
"description": "有効にすると古い絵文字を削除し、無効にすると新しい絵文字を収集しません"
|
||||
},
|
||||
"checkInterval": {
|
||||
"label": "確認間隔(分)",
|
||||
"description": "絵文字の登録、破損、削除を確認する間隔です"
|
||||
},
|
||||
"stealEmoji": {
|
||||
"label": "絵文字を収集する",
|
||||
"description": "一部の絵文字をボットが自分用に保存できるようにします"
|
||||
},
|
||||
"contentFiltration": {
|
||||
"label": "絵文字フィルタリングを有効にする",
|
||||
"description": "条件に合う絵文字だけを保存します"
|
||||
},
|
||||
"filtrationPrompt": {
|
||||
"label": "フィルタ条件",
|
||||
"placeholder": "例:公序良俗に反しないこと",
|
||||
"description": "保存する絵文字の条件を説明してください"
|
||||
}
|
||||
},
|
||||
"other": {
|
||||
"enableTool": {
|
||||
"label": "ツールシステムを有効にする",
|
||||
"description": "ボットが各種ツールを使って機能を拡張できるようにします"
|
||||
},
|
||||
"allGlobal": {
|
||||
"label": "グローバルスラングモードを有効にする",
|
||||
"description": "グループ内のスラングを学習して使えるようにします"
|
||||
}
|
||||
},
|
||||
"siliconFlow": {
|
||||
"about": {
|
||||
"title": "SiliconFlow について",
|
||||
"description": "SiliconFlow は DeepSeek V3、Qwen、ビジョンモデル、音声認識、埋め込みモデルなど幅広いモデルを提供します。API Key が1つあれば MaiBot の全機能を利用できます。",
|
||||
"link": "SiliconFlow で API Key を取得する"
|
||||
},
|
||||
"apiKey": {
|
||||
"label": "SiliconFlow API Key *",
|
||||
"description": "SiliconFlow の API Key を入力してください。入力後、MaiBot が必要なモデルを自動設定します。",
|
||||
"show": "API Key を表示",
|
||||
"hide": "API Key を隠す"
|
||||
},
|
||||
"autoConfig": {
|
||||
"title": "以下のモデルが自動設定されます:",
|
||||
"items": {
|
||||
"deepseek": "DeepSeek V3 - メインの会話・ツールモデル",
|
||||
"qwen3": "Qwen3 30B - 頻繁な小タスクとツール呼び出し",
|
||||
"qwen3Vl": "Qwen3 VL 30B - 画像認識",
|
||||
"senseVoice": "SenseVoice - 音声認識",
|
||||
"bgeM3": "BGE-M3 - テキスト埋め込み",
|
||||
"lpmm": "知識ベース関連モデル (LPMM)"
|
||||
}
|
||||
},
|
||||
"hint": {
|
||||
"title": "ヒント:",
|
||||
"description": "ウィザード完了後は、「システム設定 -> モデル設定」でさらに API プロバイダーやモデルを追加できます。"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"loading": "読み込み中...",
|
||||
"error": "エラー",
|
||||
|
||||
@@ -379,6 +379,220 @@
|
||||
"switchToLight": "라이트 모드로 전환",
|
||||
"switchToDark": "다크 모드로 전환"
|
||||
},
|
||||
"setupPage": {
|
||||
"steps": {
|
||||
"botBasic": {
|
||||
"title": "Bot 기본 설정",
|
||||
"description": "봇의 기본 정보를 설정합니다"
|
||||
},
|
||||
"personality": {
|
||||
"title": "성격 설정",
|
||||
"description": "봇의 성격과 말투를 정의합니다"
|
||||
},
|
||||
"emoji": {
|
||||
"title": "이모지 팩",
|
||||
"description": "이모지 관련 설정을 구성합니다"
|
||||
},
|
||||
"other": {
|
||||
"title": "기타 설정",
|
||||
"description": "도구, 감정 시스템 등의 설정을 구성합니다"
|
||||
},
|
||||
"siliconFlow": {
|
||||
"title": "API 설정",
|
||||
"description": "SiliconFlow API 키를 설정합니다"
|
||||
}
|
||||
},
|
||||
"loading": {
|
||||
"title": "설정을 불러오는 중...",
|
||||
"description": "기존 설정을 읽고 있습니다"
|
||||
},
|
||||
"header": {
|
||||
"title": "초기 설정 마법사",
|
||||
"description": "{{appName}}의 초기 설정을 함께 완료해 봅시다"
|
||||
},
|
||||
"progress": {
|
||||
"stepCounter": "단계 {{current}} / {{total}}"
|
||||
},
|
||||
"validation": {
|
||||
"selectPlatform": "플랫폼을 선택해 주세요",
|
||||
"enterNickname": "닉네임을 입력해 주세요",
|
||||
"enterQqAccount": "QQ 계정을 입력해 주세요",
|
||||
"enterAccountId": "계정 ID를 입력해 주세요"
|
||||
},
|
||||
"toast": {
|
||||
"loadFailedTitle": "설정 불러오기에 실패했습니다",
|
||||
"loadFailedDescription": "기존 설정을 불러올 수 없어 기본값을 사용합니다",
|
||||
"saveSuccessTitle": "저장되었습니다",
|
||||
"saveSuccessDescription": "{{step}} 설정이 저장되었습니다",
|
||||
"saveFailedTitle": "저장에 실패했습니다",
|
||||
"validationFailedTitle": "입력값을 확인해 주세요",
|
||||
"completeSuccessTitle": "설정이 완료되었습니다",
|
||||
"completeSuccessDescription": "새 설정을 적용하기 위해 {{appName}}을(를) 재시작하는 중입니다...",
|
||||
"completeFailedTitle": "설정 완료에 실패했습니다",
|
||||
"skipFailedTitle": "설정 마법사 건너뛰기에 실패했습니다",
|
||||
"unknownError": "알 수 없는 오류"
|
||||
},
|
||||
"actions": {
|
||||
"previous": "이전",
|
||||
"next": "다음",
|
||||
"skip": "마법사 건너뛰기",
|
||||
"complete": "설정 완료",
|
||||
"saving": "저장 중...",
|
||||
"completing": "완료 중..."
|
||||
},
|
||||
"skipDialog": {
|
||||
"title": "설정 마법사를 건너뛸까요?",
|
||||
"description": "시스템 설정에서 언제든지 다시 설정 마법사를 열 수 있습니다. 정말 건너뛰시겠습니까?",
|
||||
"confirm": "건너뛰기"
|
||||
},
|
||||
"footer": "이 설정들은 언제든지 설정 화면에서 변경할 수 있습니다",
|
||||
"defaults": {
|
||||
"personality": {
|
||||
"personality": "여자 대학생이며 현재 2학년이고, Tieba 같은 커뮤니티를 자주 봅니다.",
|
||||
"replyStyle": "답변은 담백하고 짧게 하며 중국어로 말하세요. 자신의 학과 배경을 일부러 강조하지 마세요. Tieba, Zhihu, Weibo의 답변 스타일을 참고할 수 있습니다.",
|
||||
"interest": "기술, 게임, 애니메이션, 일상적인 주제에 관심이 있고, 너무 무겁거나 엄숙한 주제는 좋아하지 않습니다.",
|
||||
"planStyle": "1. 사용 가능한 **모든** action 의 **각 동작** 이 현재 조건에 맞는지 검토하고, 대화 내용에 맞으면 사용하세요\n2. 같은 내용이 이미 실행되었다면 반복하지 마세요\n3. 발화 빈도를 조절하고 너무 자주 말하지 마세요\n4. 누군가 당신을 귀찮아하는 것 같다면 답장을 줄이세요\n5. 누군가 당신을 공격하거나 감정적으로 반응하면 적절하게 대응하세요",
|
||||
"privatePlanStyle": "1. 사용 가능한 **모든** action 의 **각 동작** 이 현재 조건에 맞는지 검토하고, 대화 내용에 맞으면 사용하세요\n2. 같은 내용이 이미 실행되었다면 반복하지 마세요\n3. 이미 답한 문장에는 다시 답하지 마세요"
|
||||
},
|
||||
"emoji": {
|
||||
"filtrationPrompt": "공공질서와 미풍양속에 어긋나지 않음"
|
||||
}
|
||||
},
|
||||
"forms": {
|
||||
"botBasic": {
|
||||
"platform": {
|
||||
"label": "플랫폼 *",
|
||||
"placeholder": "플랫폼 선택",
|
||||
"description": "봇이 실행될 플랫폼을 선택합니다",
|
||||
"options": {
|
||||
"custom": "기타 플랫폼"
|
||||
}
|
||||
},
|
||||
"customPlatform": {
|
||||
"label": "플랫폼 이름 *",
|
||||
"placeholder": "예: matrix 와 같은 플랫폼 이름 입력"
|
||||
},
|
||||
"qqAccount": {
|
||||
"label": "QQ 계정 *",
|
||||
"placeholder": "봇의 QQ 계정을 입력하세요",
|
||||
"description": "봇 로그인에 사용하는 QQ 계정입니다"
|
||||
},
|
||||
"primaryAccount": {
|
||||
"label": "계정 ID *",
|
||||
"placeholder": "봇의 계정 ID를 입력하세요",
|
||||
"description": "이 플랫폼에서 봇을 식별하는 계정 ID입니다"
|
||||
},
|
||||
"nickname": {
|
||||
"label": "닉네임 *",
|
||||
"placeholder": "봇의 닉네임을 입력하세요",
|
||||
"description": "봇의 대표 호칭입니다"
|
||||
},
|
||||
"alias": {
|
||||
"label": "별칭",
|
||||
"placeholder": "별칭을 입력하고 Enter 로 추가",
|
||||
"add": "추가",
|
||||
"description": "봇의 다른 호칭을 여러 개 추가할 수 있습니다",
|
||||
"remove": "별칭 {{alias}} 제거"
|
||||
}
|
||||
},
|
||||
"personality": {
|
||||
"personality": {
|
||||
"label": "성격 특성 *",
|
||||
"placeholder": "봇의 성격과 설정을 설명해 주세요 (권장 120자 이내)",
|
||||
"description": "예: 대학 2학년 여자 대학생으로 Tieba 를 자주 본다"
|
||||
},
|
||||
"replyStyle": {
|
||||
"label": "말투 *",
|
||||
"placeholder": "봇이 말하는 방식과 표현 습관을 설명해 주세요",
|
||||
"description": "예: 답변은 담백하고 짧게, 중국어로 말하며 Tieba, Zhihu, Weibo 스타일을 참고한다"
|
||||
},
|
||||
"interest": {
|
||||
"label": "관심사 *",
|
||||
"placeholder": "봇이 관심을 가지는 주제를 설명해 주세요",
|
||||
"description": "어떤 주제에 더 잘 반응할지에 영향을 줍니다"
|
||||
},
|
||||
"planStyle": {
|
||||
"label": "그룹 채팅 규칙 *",
|
||||
"placeholder": "그룹 채팅에서의 행동 스타일과 규칙",
|
||||
"description": "답장 빈도와 조건 등 그룹 채팅에서의 행동 방식을 정의합니다"
|
||||
},
|
||||
"privatePlanStyle": {
|
||||
"label": "개인 채팅 규칙 *",
|
||||
"placeholder": "개인 채팅에서의 행동 스타일과 규칙",
|
||||
"description": "개인 채팅에서의 행동 방식을 정의합니다"
|
||||
}
|
||||
},
|
||||
"emoji": {
|
||||
"emojiChance": {
|
||||
"label": "이모지 활성화 확률",
|
||||
"description": "봇이 이모지를 보낼 확률입니다"
|
||||
},
|
||||
"maxRegNum": {
|
||||
"label": "최대 이모지 수",
|
||||
"description": "봇이 저장할 수 있는 이모지의 최대 개수입니다"
|
||||
},
|
||||
"doReplace": {
|
||||
"label": "최대 수 도달 시 교체",
|
||||
"description": "켜면 오래된 이모지를 삭제하고, 끄면 새 이모지를 더 이상 수집하지 않습니다"
|
||||
},
|
||||
"checkInterval": {
|
||||
"label": "확인 간격 (분)",
|
||||
"description": "이모지 등록, 손상, 삭제를 확인하는 간격입니다"
|
||||
},
|
||||
"stealEmoji": {
|
||||
"label": "이모지 수집",
|
||||
"description": "일부 이모지를 봇이 자신의 것으로 저장할 수 있게 합니다"
|
||||
},
|
||||
"contentFiltration": {
|
||||
"label": "이모지 필터링 사용",
|
||||
"description": "조건에 맞는 이모지만 저장합니다"
|
||||
},
|
||||
"filtrationPrompt": {
|
||||
"label": "필터 조건",
|
||||
"placeholder": "예: 공공질서와 미풍양속에 어긋나지 않음",
|
||||
"description": "저장할 이모지가 충족해야 하는 조건을 설명해 주세요"
|
||||
}
|
||||
},
|
||||
"other": {
|
||||
"enableTool": {
|
||||
"label": "도구 시스템 사용",
|
||||
"description": "봇이 다양한 도구를 사용해 기능을 확장할 수 있게 합니다"
|
||||
},
|
||||
"allGlobal": {
|
||||
"label": "전역 슬랭 모드 사용",
|
||||
"description": "봇이 그룹 슬랭을 학습하고 사용할 수 있게 합니다"
|
||||
}
|
||||
},
|
||||
"siliconFlow": {
|
||||
"about": {
|
||||
"title": "SiliconFlow 소개",
|
||||
"description": "SiliconFlow 는 DeepSeek V3, Qwen, 비전 모델, 음성 인식, 임베딩 모델 등 폭넓은 모델을 제공합니다. API Key 하나로 MaiBot 의 모든 기능을 사용할 수 있습니다.",
|
||||
"link": "SiliconFlow 에서 API Key 받기"
|
||||
},
|
||||
"apiKey": {
|
||||
"label": "SiliconFlow API Key *",
|
||||
"description": "SiliconFlow API Key를 입력해 주세요. 입력하면 MaiBot 이 필요한 모델을 자동으로 구성합니다.",
|
||||
"show": "API Key 표시",
|
||||
"hide": "API Key 숨기기"
|
||||
},
|
||||
"autoConfig": {
|
||||
"title": "다음 모델이 자동으로 구성됩니다:",
|
||||
"items": {
|
||||
"deepseek": "DeepSeek V3 - 주요 대화 및 도구 모델",
|
||||
"qwen3": "Qwen3 30B - 잦은 소규모 작업과 도구 호출",
|
||||
"qwen3Vl": "Qwen3 VL 30B - 이미지 인식",
|
||||
"senseVoice": "SenseVoice - 음성 인식",
|
||||
"bgeM3": "BGE-M3 - 텍스트 임베딩",
|
||||
"lpmm": "지식 베이스 관련 모델 (LPMM)"
|
||||
}
|
||||
},
|
||||
"hint": {
|
||||
"title": "팁: ",
|
||||
"description": "마법사를 마친 뒤에는 \"시스템 설정 -> 모델 설정\"에서 더 많은 API 제공자와 모델을 추가할 수 있습니다."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"loading": "로딩 중...",
|
||||
"error": "오류",
|
||||
|
||||
@@ -379,6 +379,220 @@
|
||||
"switchToLight": "切换到浅色模式",
|
||||
"switchToDark": "切换到深色模式"
|
||||
},
|
||||
"setupPage": {
|
||||
"steps": {
|
||||
"botBasic": {
|
||||
"title": "Bot基础",
|
||||
"description": "配置机器人的基本信息"
|
||||
},
|
||||
"personality": {
|
||||
"title": "人格配置",
|
||||
"description": "定义机器人的性格和说话风格"
|
||||
},
|
||||
"emoji": {
|
||||
"title": "表情包",
|
||||
"description": "配置表情包相关设置"
|
||||
},
|
||||
"other": {
|
||||
"title": "其他设置",
|
||||
"description": "工具、情绪系统等配置"
|
||||
},
|
||||
"siliconFlow": {
|
||||
"title": "API配置",
|
||||
"description": "配置硅基流动 API 密钥"
|
||||
}
|
||||
},
|
||||
"loading": {
|
||||
"title": "加载配置中...",
|
||||
"description": "正在读取现有配置"
|
||||
},
|
||||
"header": {
|
||||
"title": "首次配置向导",
|
||||
"description": "让我们一起完成 {{appName}} 的初始配置"
|
||||
},
|
||||
"progress": {
|
||||
"stepCounter": "步骤 {{current}} / {{total}}"
|
||||
},
|
||||
"validation": {
|
||||
"selectPlatform": "请选择平台",
|
||||
"enterNickname": "请输入昵称",
|
||||
"enterQqAccount": "请输入 QQ 账号",
|
||||
"enterAccountId": "请输入账号 ID"
|
||||
},
|
||||
"toast": {
|
||||
"loadFailedTitle": "加载配置失败",
|
||||
"loadFailedDescription": "无法加载现有配置,将使用默认值",
|
||||
"saveSuccessTitle": "保存成功",
|
||||
"saveSuccessDescription": "{{step}} 配置已保存",
|
||||
"saveFailedTitle": "保存失败",
|
||||
"validationFailedTitle": "验证失败",
|
||||
"completeSuccessTitle": "配置完成",
|
||||
"completeSuccessDescription": "{{appName}} 正在重启以应用新配置...",
|
||||
"completeFailedTitle": "配置失败",
|
||||
"skipFailedTitle": "跳过失败",
|
||||
"unknownError": "未知错误"
|
||||
},
|
||||
"actions": {
|
||||
"previous": "上一步",
|
||||
"next": "下一步",
|
||||
"skip": "跳过向导",
|
||||
"complete": "完成配置",
|
||||
"saving": "保存中...",
|
||||
"completing": "完成中..."
|
||||
},
|
||||
"skipDialog": {
|
||||
"title": "确认跳过配置向导",
|
||||
"description": "您可以随时在系统设置中重新进入配置向导。确定要跳过吗?",
|
||||
"confirm": "确认跳过"
|
||||
},
|
||||
"footer": "您可以随时在设置中修改这些配置",
|
||||
"defaults": {
|
||||
"personality": {
|
||||
"personality": "是一个女大学生,现在在读大二,会刷贴吧。",
|
||||
"replyStyle": "请回复得平淡一些,简短一些,说中文,不要刻意突出自身学科背景。可以参考贴吧、知乎和微博的回复风格。",
|
||||
"interest": "对技术相关话题、游戏和动漫相关话题感兴趣,也对日常话题感兴趣,不喜欢太过沉重严肃的话题。",
|
||||
"planStyle": "1.思考**所有**的可用的 action 中的**每个动作**是否符合当下条件,如果动作使用条件符合聊天内容就使用\n2.如果相同的内容已经被执行,请不要重复执行\n3.请控制你的发言频率,不要太过频繁地发言\n4.如果有人对你感到厌烦,请减少回复\n5.如果有人对你进行攻击,或者情绪激动,请你以合适的方法应对",
|
||||
"privatePlanStyle": "1.思考**所有**的可用的 action 中的**每个动作**是否符合当下条件,如果动作使用条件符合聊天内容就使用\n2.如果相同的内容已经被执行,请不要重复执行\n3.某句话如果已经被回复过,不要重复回复"
|
||||
},
|
||||
"emoji": {
|
||||
"filtrationPrompt": "符合公序良俗"
|
||||
}
|
||||
},
|
||||
"forms": {
|
||||
"botBasic": {
|
||||
"platform": {
|
||||
"label": "平台 *",
|
||||
"placeholder": "请选择平台",
|
||||
"description": "选择机器人运行的平台",
|
||||
"options": {
|
||||
"custom": "其他平台"
|
||||
}
|
||||
},
|
||||
"customPlatform": {
|
||||
"label": "平台名称 *",
|
||||
"placeholder": "请输入平台名称,如 matrix"
|
||||
},
|
||||
"qqAccount": {
|
||||
"label": "QQ账号 *",
|
||||
"placeholder": "请输入机器人的 QQ 账号",
|
||||
"description": "机器人登录使用的 QQ 账号"
|
||||
},
|
||||
"primaryAccount": {
|
||||
"label": "账号 ID *",
|
||||
"placeholder": "请输入机器人的账号 ID",
|
||||
"description": "机器人在该平台上的账号标识"
|
||||
},
|
||||
"nickname": {
|
||||
"label": "昵称 *",
|
||||
"placeholder": "请输入机器人的昵称",
|
||||
"description": "机器人的主要称呼名称"
|
||||
},
|
||||
"alias": {
|
||||
"label": "别名",
|
||||
"placeholder": "输入别名后按回车添加",
|
||||
"add": "添加",
|
||||
"description": "机器人的其他称呼,可以添加多个",
|
||||
"remove": "移除别名 {{alias}}"
|
||||
}
|
||||
},
|
||||
"personality": {
|
||||
"personality": {
|
||||
"label": "人格特征 *",
|
||||
"placeholder": "描述机器人的人格特质和身份特征(建议 120 字以内)",
|
||||
"description": "例如:是一个女大学生,现在在读大二,会刷贴吧"
|
||||
},
|
||||
"replyStyle": {
|
||||
"label": "表达风格 *",
|
||||
"placeholder": "描述机器人说话的表达风格、表达习惯",
|
||||
"description": "例如:回复平淡一些,简短一些,说中文,参考贴吧、知乎和微博的回复风格"
|
||||
},
|
||||
"interest": {
|
||||
"label": "兴趣 *",
|
||||
"placeholder": "描述机器人感兴趣的话题",
|
||||
"description": "会影响机器人对什么话题进行回复"
|
||||
},
|
||||
"planStyle": {
|
||||
"label": "群聊说话规则 *",
|
||||
"placeholder": "机器人在群聊中的行为风格和规则",
|
||||
"description": "定义机器人在群聊中如何行动,例如回复频率、条件等"
|
||||
},
|
||||
"privatePlanStyle": {
|
||||
"label": "私聊说话规则 *",
|
||||
"placeholder": "机器人在私聊中的行为风格和规则",
|
||||
"description": "定义机器人在私聊中的行为方式"
|
||||
}
|
||||
},
|
||||
"emoji": {
|
||||
"emojiChance": {
|
||||
"label": "表情包激活概率",
|
||||
"description": "机器人发送表情包的概率"
|
||||
},
|
||||
"maxRegNum": {
|
||||
"label": "最大表情包数量",
|
||||
"description": "机器人最多保存的表情包数量"
|
||||
},
|
||||
"doReplace": {
|
||||
"label": "达到最大数量时替换",
|
||||
"description": "开启后会删除旧表情包,关闭则不再收集新表情包"
|
||||
},
|
||||
"checkInterval": {
|
||||
"label": "检查间隔(分钟)",
|
||||
"description": "检查表情包注册、破损、删除的时间间隔"
|
||||
},
|
||||
"stealEmoji": {
|
||||
"label": "偷取表情包",
|
||||
"description": "允许机器人将一些表情包据为己有"
|
||||
},
|
||||
"contentFiltration": {
|
||||
"label": "启用表情包过滤",
|
||||
"description": "只保存符合要求的表情包"
|
||||
},
|
||||
"filtrationPrompt": {
|
||||
"label": "过滤要求",
|
||||
"placeholder": "例如:符合公序良俗",
|
||||
"description": "描述表情包应该符合的要求"
|
||||
}
|
||||
},
|
||||
"other": {
|
||||
"enableTool": {
|
||||
"label": "启用工具系统",
|
||||
"description": "允许机器人使用各种工具增强功能"
|
||||
},
|
||||
"allGlobal": {
|
||||
"label": "启用全局黑话模式",
|
||||
"description": "允许机器人学习和使用群组黑话"
|
||||
}
|
||||
},
|
||||
"siliconFlow": {
|
||||
"about": {
|
||||
"title": "关于硅基流动 (SiliconFlow)",
|
||||
"description": "硅基流动提供了完整的模型覆盖,包括 DeepSeek V3、Qwen、视觉模型、语音识别和嵌入模型。只需一个 API Key 即可使用麦麦的所有功能!",
|
||||
"link": "前往硅基流动获取 API Key"
|
||||
},
|
||||
"apiKey": {
|
||||
"label": "SiliconFlow API Key *",
|
||||
"description": "请输入您的硅基流动 API 密钥。获取后,麦麦将自动配置所有必需的模型。",
|
||||
"show": "显示 API Key",
|
||||
"hide": "隐藏 API Key"
|
||||
},
|
||||
"autoConfig": {
|
||||
"title": "将自动配置以下模型:",
|
||||
"items": {
|
||||
"deepseek": "DeepSeek V3 - 主要对话和工具模型",
|
||||
"qwen3": "Qwen3 30B - 高频小任务和工具调用",
|
||||
"qwen3Vl": "Qwen3 VL 30B - 图像识别",
|
||||
"senseVoice": "SenseVoice - 语音识别",
|
||||
"bgeM3": "BGE-M3 - 文本嵌入",
|
||||
"lpmm": "知识库相关模型 (LPMM)"
|
||||
}
|
||||
},
|
||||
"hint": {
|
||||
"title": "💡 提示:",
|
||||
"description": "完成向导后,您可以在“系统设置 -> 模型配置”中添加更多 API 提供商和模型。"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"loading": "加载中...",
|
||||
"error": "错误",
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
// 设置向导各步骤表单组件
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import { ExternalLink, Eye, EyeOff, X } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -15,12 +15,15 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
import { X, ExternalLink, Eye, EyeOff } from 'lucide-react'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
|
||||
import type {
|
||||
BotBasicConfig,
|
||||
PersonalityConfig,
|
||||
EmojiConfig,
|
||||
OtherBasicConfig,
|
||||
PersonalityConfig,
|
||||
SiliconFlowConfig,
|
||||
} from './types'
|
||||
|
||||
@@ -34,13 +37,7 @@ const KNOWN_PLATFORMS: Record<string, string> = {
|
||||
kook: 'kook',
|
||||
}
|
||||
|
||||
const PLATFORM_OPTIONS = [
|
||||
{ value: 'qq', label: 'QQ' },
|
||||
{ value: 'telegram', label: 'Telegram' },
|
||||
{ value: 'discord', label: 'Discord' },
|
||||
{ value: 'kook', label: 'Kook' },
|
||||
{ value: 'custom', label: '其他平台' },
|
||||
]
|
||||
const PLATFORM_OPTIONS = ['qq', 'telegram', 'discord', 'kook', 'custom'] as const
|
||||
|
||||
function normalizePlatform(raw: string): string {
|
||||
const key = raw.trim().toLowerCase()
|
||||
@@ -56,17 +53,21 @@ function deriveSelectedPlatform(config: BotBasicConfig): { selected: string; cus
|
||||
if (!platform) {
|
||||
return { selected: '', customName: '' }
|
||||
}
|
||||
const known = PLATFORM_OPTIONS.find((opt) => opt.value === platform && opt.value !== 'custom')
|
||||
const known = PLATFORM_OPTIONS.find((value) => value === platform && value !== 'custom')
|
||||
if (known) {
|
||||
return { selected: platform, customName: '' }
|
||||
}
|
||||
return { selected: 'custom', customName: platform }
|
||||
}
|
||||
|
||||
function upsertPlatformAccount(platforms: string[], platformName: string, accountId: string): string[] {
|
||||
function upsertPlatformAccount(
|
||||
platforms: string[],
|
||||
platformName: string,
|
||||
accountId: string
|
||||
): string[] {
|
||||
const normalized = normalizePlatform(platformName)
|
||||
const filtered = platforms.filter((p) => {
|
||||
const prefix = p.split(':')[0]
|
||||
const filtered = platforms.filter((platform) => {
|
||||
const prefix = platform.split(':')[0]
|
||||
return normalizePlatform(prefix) !== normalized
|
||||
})
|
||||
if (accountId.trim()) {
|
||||
@@ -77,8 +78,8 @@ function upsertPlatformAccount(platforms: string[], platformName: string, accoun
|
||||
|
||||
function getPrimaryAccount(platforms: string[], platformName: string): string {
|
||||
const normalized = normalizePlatform(platformName)
|
||||
const entry = platforms.find((p) => {
|
||||
const prefix = p.split(':')[0]
|
||||
const entry = platforms.find((platform) => {
|
||||
const prefix = platform.split(':')[0]
|
||||
return normalizePlatform(prefix) === normalized
|
||||
})
|
||||
return entry ? entry.split(':').slice(1).join(':') : ''
|
||||
@@ -90,58 +91,53 @@ interface BotBasicFormProps {
|
||||
}
|
||||
|
||||
export function BotBasicForm({ config, onChange }: BotBasicFormProps) {
|
||||
const { t } = useTranslation()
|
||||
const derived = deriveSelectedPlatform(config)
|
||||
const [selectedPlatform, setSelectedPlatform] = useState(derived.selected)
|
||||
const [customPlatformName, setCustomPlatformName] = useState(derived.customName)
|
||||
const [primaryAccount, setPrimaryAccount] = useState(() => {
|
||||
if (derived.selected === 'qq') {
|
||||
return config.qq_account > 0 ? String(config.qq_account) : ''
|
||||
}
|
||||
if (config.platform) {
|
||||
return getPrimaryAccount(config.platforms, config.platform)
|
||||
}
|
||||
return ''
|
||||
})
|
||||
const [selectedPlatformOverride, setSelectedPlatformOverride] = useState<string | null>(null)
|
||||
const [customPlatformNameOverride, setCustomPlatformNameOverride] = useState<string | null>(null)
|
||||
const selectedPlatform = selectedPlatformOverride ?? derived.selected
|
||||
const customPlatformName = customPlatformNameOverride ?? derived.customName
|
||||
const primaryAccount =
|
||||
selectedPlatform === 'qq'
|
||||
? config.qq_account > 0
|
||||
? String(config.qq_account)
|
||||
: ''
|
||||
: config.platform
|
||||
? getPrimaryAccount(config.platforms, config.platform)
|
||||
: ''
|
||||
|
||||
// Re-derive when config loads from API (e.g. after initial fetch)
|
||||
useEffect(() => {
|
||||
const d = deriveSelectedPlatform(config)
|
||||
setSelectedPlatform(d.selected)
|
||||
setCustomPlatformName(d.customName)
|
||||
if (d.selected === 'qq') {
|
||||
setPrimaryAccount(config.qq_account > 0 ? String(config.qq_account) : '')
|
||||
} else if (config.platform) {
|
||||
setPrimaryAccount(getPrimaryAccount(config.platforms, config.platform))
|
||||
}
|
||||
}, [config.platform, config.qq_account, config.platforms])
|
||||
const platformOptions = [
|
||||
{ value: 'qq', label: 'QQ' },
|
||||
{ value: 'telegram', label: 'Telegram' },
|
||||
{ value: 'discord', label: 'Discord' },
|
||||
{ value: 'kook', label: 'Kook' },
|
||||
{ value: 'custom', label: t('setupPage.forms.botBasic.platform.options.custom') },
|
||||
]
|
||||
|
||||
const handlePlatformChange = (value: string) => {
|
||||
setSelectedPlatform(value)
|
||||
setSelectedPlatformOverride(value)
|
||||
const realPlatform = value === 'custom' ? customPlatformName : value
|
||||
setPrimaryAccount('')
|
||||
onChange({
|
||||
...config,
|
||||
platform: normalizePlatform(realPlatform),
|
||||
qq_account: value === 'qq' ? config.qq_account : config.qq_account, // preserve
|
||||
qq_account: value === 'qq' ? config.qq_account : config.qq_account,
|
||||
})
|
||||
}
|
||||
|
||||
const handleCustomNameChange = (name: string) => {
|
||||
setCustomPlatformName(name)
|
||||
setCustomPlatformNameOverride(name)
|
||||
const normalized = normalizePlatform(name)
|
||||
// Move account to new platform name if we had one
|
||||
const newPlatforms = primaryAccount
|
||||
const nextPlatforms = primaryAccount
|
||||
? upsertPlatformAccount(config.platforms, normalized, primaryAccount)
|
||||
: config.platforms
|
||||
onChange({
|
||||
...config,
|
||||
platform: normalized,
|
||||
platforms: newPlatforms,
|
||||
platforms: nextPlatforms,
|
||||
})
|
||||
}
|
||||
|
||||
const handleAccountChange = (accountId: string) => {
|
||||
setPrimaryAccount(accountId)
|
||||
const realPlatform = selectedPlatform === 'custom' ? customPlatformName : selectedPlatform
|
||||
const normalized = normalizePlatform(realPlatform)
|
||||
|
||||
@@ -172,37 +168,39 @@ export function BotBasicForm({ config, onChange }: BotBasicFormProps) {
|
||||
const handleRemoveAlias = (index: number) => {
|
||||
onChange({
|
||||
...config,
|
||||
alias_names: config.alias_names.filter((_, i) => i !== index),
|
||||
alias_names: config.alias_names.filter((_, aliasIndex) => aliasIndex !== index),
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-3">
|
||||
<Label htmlFor="platform">平台 *</Label>
|
||||
<Label htmlFor="platform">{t('setupPage.forms.botBasic.platform.label')}</Label>
|
||||
<Select value={selectedPlatform} onValueChange={handlePlatformChange}>
|
||||
<SelectTrigger id="platform">
|
||||
<SelectValue placeholder="请选择平台" />
|
||||
<SelectValue placeholder={t('setupPage.forms.botBasic.platform.placeholder')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{PLATFORM_OPTIONS.map((opt) => (
|
||||
<SelectItem key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
{platformOptions.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
选择机器人运行的平台
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{t('setupPage.forms.botBasic.platform.description')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{selectedPlatform === 'custom' && (
|
||||
<div className="space-y-3">
|
||||
<Label htmlFor="custom_platform_name">平台名称 *</Label>
|
||||
<Label htmlFor="custom_platform_name">
|
||||
{t('setupPage.forms.botBasic.customPlatform.label')}
|
||||
</Label>
|
||||
<Input
|
||||
id="custom_platform_name"
|
||||
placeholder="请输入平台名称,如 matrix"
|
||||
placeholder={t('setupPage.forms.botBasic.customPlatform.placeholder')}
|
||||
value={customPlatformName}
|
||||
onChange={(e) => handleCustomNameChange(e.target.value)}
|
||||
/>
|
||||
@@ -211,58 +209,63 @@ export function BotBasicForm({ config, onChange }: BotBasicFormProps) {
|
||||
|
||||
{selectedPlatform === 'qq' && (
|
||||
<div className="space-y-3">
|
||||
<Label htmlFor="qq_account">QQ账号 *</Label>
|
||||
<Label htmlFor="qq_account">{t('setupPage.forms.botBasic.qqAccount.label')}</Label>
|
||||
<Input
|
||||
id="qq_account"
|
||||
type="number"
|
||||
placeholder="请输入机器人的QQ账号"
|
||||
placeholder={t('setupPage.forms.botBasic.qqAccount.placeholder')}
|
||||
value={primaryAccount}
|
||||
onChange={(e) => handleAccountChange(e.target.value)}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
机器人登录使用的QQ账号
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{t('setupPage.forms.botBasic.qqAccount.description')}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedPlatform && selectedPlatform !== 'qq' && (selectedPlatform !== 'custom' || customPlatformName) && (
|
||||
<div className="space-y-3">
|
||||
<Label htmlFor="primary_account">账号ID *</Label>
|
||||
<Input
|
||||
id="primary_account"
|
||||
placeholder="请输入机器人的账号ID"
|
||||
value={primaryAccount}
|
||||
onChange={(e) => handleAccountChange(e.target.value)}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
机器人在该平台上的账号标识
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{selectedPlatform &&
|
||||
selectedPlatform !== 'qq' &&
|
||||
(selectedPlatform !== 'custom' || customPlatformName) && (
|
||||
<div className="space-y-3">
|
||||
<Label htmlFor="primary_account">
|
||||
{t('setupPage.forms.botBasic.primaryAccount.label')}
|
||||
</Label>
|
||||
<Input
|
||||
id="primary_account"
|
||||
placeholder={t('setupPage.forms.botBasic.primaryAccount.placeholder')}
|
||||
value={primaryAccount}
|
||||
onChange={(e) => handleAccountChange(e.target.value)}
|
||||
/>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{t('setupPage.forms.botBasic.primaryAccount.description')}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-3">
|
||||
<Label htmlFor="nickname">昵称 *</Label>
|
||||
<Label htmlFor="nickname">{t('setupPage.forms.botBasic.nickname.label')}</Label>
|
||||
<Input
|
||||
id="nickname"
|
||||
placeholder="请输入机器人的昵称"
|
||||
placeholder={t('setupPage.forms.botBasic.nickname.placeholder')}
|
||||
value={config.nickname}
|
||||
onChange={(e) => onChange({ ...config, nickname: e.target.value })}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
机器人的主要称呼名称
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{t('setupPage.forms.botBasic.nickname.description')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<Label>别名</Label>
|
||||
<div className="flex flex-wrap gap-2 mb-2">
|
||||
<Label>{t('setupPage.forms.botBasic.alias.label')}</Label>
|
||||
<div className="mb-2 flex flex-wrap gap-2">
|
||||
{config.alias_names.map((alias, index) => (
|
||||
<Badge key={index} variant="secondary" className="gap-1">
|
||||
{alias}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleRemoveAlias(index)}
|
||||
className="ml-1 hover:text-destructive"
|
||||
className="hover:text-destructive ml-1"
|
||||
aria-label={t('setupPage.forms.botBasic.alias.remove', { alias })}
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</button>
|
||||
@@ -272,7 +275,7 @@ export function BotBasicForm({ config, onChange }: BotBasicFormProps) {
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
id="alias_input"
|
||||
placeholder="输入别名后按回车添加"
|
||||
placeholder={t('setupPage.forms.botBasic.alias.placeholder')}
|
||||
onKeyPress={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
handleAddAlias((e.target as HTMLInputElement).value)
|
||||
@@ -284,20 +287,18 @@ export function BotBasicForm({ config, onChange }: BotBasicFormProps) {
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
const input = document.getElementById(
|
||||
'alias_input'
|
||||
) as HTMLInputElement
|
||||
const input = document.getElementById('alias_input') as HTMLInputElement
|
||||
if (input) {
|
||||
handleAddAlias(input.value)
|
||||
input.value = ''
|
||||
}
|
||||
}}
|
||||
>
|
||||
添加
|
||||
{t('setupPage.forms.botBasic.alias.add')}
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
机器人的其他称呼,可以添加多个
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{t('setupPage.forms.botBasic.alias.description')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -311,79 +312,81 @@ interface PersonalityFormProps {
|
||||
}
|
||||
|
||||
export function PersonalityForm({ config, onChange }: PersonalityFormProps) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-3">
|
||||
<Label htmlFor="personality">人格特征 *</Label>
|
||||
<Label htmlFor="personality">{t('setupPage.forms.personality.personality.label')}</Label>
|
||||
<Textarea
|
||||
id="personality"
|
||||
placeholder="描述机器人的人格特质和身份特征(建议120字以内)"
|
||||
placeholder={t('setupPage.forms.personality.personality.placeholder')}
|
||||
value={config.personality}
|
||||
onChange={(e) => onChange({ ...config, personality: e.target.value })}
|
||||
rows={3}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
例如:是一个女大学生,现在在读大二,会刷贴吧
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{t('setupPage.forms.personality.personality.description')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<Label htmlFor="reply_style">表达风格 *</Label>
|
||||
<Label htmlFor="reply_style">{t('setupPage.forms.personality.replyStyle.label')}</Label>
|
||||
<Textarea
|
||||
id="reply_style"
|
||||
placeholder="描述机器人说话的表达风格、表达习惯"
|
||||
placeholder={t('setupPage.forms.personality.replyStyle.placeholder')}
|
||||
value={config.reply_style}
|
||||
onChange={(e) => onChange({ ...config, reply_style: e.target.value })}
|
||||
rows={3}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
例如:回复平淡一些,简短一些,说中文,参考贴吧、知乎和微博的回复风格
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{t('setupPage.forms.personality.replyStyle.description')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<Label htmlFor="interest">兴趣 *</Label>
|
||||
<Label htmlFor="interest">{t('setupPage.forms.personality.interest.label')}</Label>
|
||||
<Textarea
|
||||
id="interest"
|
||||
placeholder="描述机器人感兴趣的话题"
|
||||
placeholder={t('setupPage.forms.personality.interest.placeholder')}
|
||||
value={config.interest}
|
||||
onChange={(e) => onChange({ ...config, interest: e.target.value })}
|
||||
rows={2}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
会影响机器人对什么话题进行回复
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{t('setupPage.forms.personality.interest.description')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div className="space-y-3">
|
||||
<Label htmlFor="plan_style">群聊说话规则 *</Label>
|
||||
<Label htmlFor="plan_style">{t('setupPage.forms.personality.planStyle.label')}</Label>
|
||||
<Textarea
|
||||
id="plan_style"
|
||||
placeholder="机器人在群聊中的行为风格和规则"
|
||||
placeholder={t('setupPage.forms.personality.planStyle.placeholder')}
|
||||
value={config.plan_style}
|
||||
onChange={(e) => onChange({ ...config, plan_style: e.target.value })}
|
||||
rows={4}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
定义机器人在群聊中如何行动,例如回复频率、条件等
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{t('setupPage.forms.personality.planStyle.description')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<Label htmlFor="private_plan_style">私聊说话规则 *</Label>
|
||||
<Label htmlFor="private_plan_style">
|
||||
{t('setupPage.forms.personality.privatePlanStyle.label')}
|
||||
</Label>
|
||||
<Textarea
|
||||
id="private_plan_style"
|
||||
placeholder="机器人在私聊中的行为风格和规则"
|
||||
placeholder={t('setupPage.forms.personality.privatePlanStyle.placeholder')}
|
||||
value={config.private_plan_style}
|
||||
onChange={(e) =>
|
||||
onChange({ ...config, private_plan_style: e.target.value })
|
||||
}
|
||||
onChange={(e) => onChange({ ...config, private_plan_style: e.target.value })}
|
||||
rows={3}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
定义机器人在私聊中的行为方式
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{t('setupPage.forms.personality.privatePlanStyle.description')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -397,12 +400,14 @@ interface EmojiFormProps {
|
||||
}
|
||||
|
||||
export function EmojiForm({ config, onChange }: EmojiFormProps) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="emoji_chance">表情包激活概率</Label>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
<Label htmlFor="emoji_chance">{t('setupPage.forms.emoji.emojiChance.label')}</Label>
|
||||
<span className="text-muted-foreground text-sm">
|
||||
{(config.emoji_chance * 100).toFixed(0)}%
|
||||
</span>
|
||||
</div>
|
||||
@@ -413,62 +418,54 @@ export function EmojiForm({ config, onChange }: EmojiFormProps) {
|
||||
max="1"
|
||||
step="0.1"
|
||||
value={config.emoji_chance}
|
||||
onChange={(e) =>
|
||||
onChange({ ...config, emoji_chance: Number(e.target.value) })
|
||||
}
|
||||
onChange={(e) => onChange({ ...config, emoji_chance: Number(e.target.value) })}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
机器人发送表情包的概率
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{t('setupPage.forms.emoji.emojiChance.description')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<Label htmlFor="max_reg_num">最大表情包数量</Label>
|
||||
<Label htmlFor="max_reg_num">{t('setupPage.forms.emoji.maxRegNum.label')}</Label>
|
||||
<Input
|
||||
id="max_reg_num"
|
||||
type="number"
|
||||
min="1"
|
||||
max="200"
|
||||
value={config.max_reg_num}
|
||||
onChange={(e) =>
|
||||
onChange({ ...config, max_reg_num: Number(e.target.value) })
|
||||
}
|
||||
onChange={(e) => onChange({ ...config, max_reg_num: Number(e.target.value) })}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
机器人最多保存的表情包数量
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{t('setupPage.forms.emoji.maxRegNum.description')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="do_replace">达到最大数量时替换</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
开启后会删除旧表情包,关闭则不再收集新表情包
|
||||
<Label htmlFor="do_replace">{t('setupPage.forms.emoji.doReplace.label')}</Label>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{t('setupPage.forms.emoji.doReplace.description')}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
id="do_replace"
|
||||
checked={config.do_replace}
|
||||
onCheckedChange={(checked) =>
|
||||
onChange({ ...config, do_replace: checked })
|
||||
}
|
||||
onCheckedChange={(checked) => onChange({ ...config, do_replace: checked })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<Label htmlFor="check_interval">检查间隔(分钟)</Label>
|
||||
<Label htmlFor="check_interval">{t('setupPage.forms.emoji.checkInterval.label')}</Label>
|
||||
<Input
|
||||
id="check_interval"
|
||||
type="number"
|
||||
min="1"
|
||||
max="120"
|
||||
value={config.check_interval}
|
||||
onChange={(e) =>
|
||||
onChange({ ...config, check_interval: Number(e.target.value) })
|
||||
}
|
||||
onChange={(e) => onChange({ ...config, check_interval: Number(e.target.value) })}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
检查表情包注册、破损、删除的时间间隔
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{t('setupPage.forms.emoji.checkInterval.description')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -476,49 +473,47 @@ export function EmojiForm({ config, onChange }: EmojiFormProps) {
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="steal_emoji">偷取表情包</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
允许机器人将一些表情包据为己有
|
||||
<Label htmlFor="steal_emoji">{t('setupPage.forms.emoji.stealEmoji.label')}</Label>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{t('setupPage.forms.emoji.stealEmoji.description')}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
id="steal_emoji"
|
||||
checked={config.steal_emoji}
|
||||
onCheckedChange={(checked) =>
|
||||
onChange({ ...config, steal_emoji: checked })
|
||||
}
|
||||
onCheckedChange={(checked) => onChange({ ...config, steal_emoji: checked })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="content_filtration">启用表情包过滤</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
只保存符合要求的表情包
|
||||
<Label htmlFor="content_filtration">
|
||||
{t('setupPage.forms.emoji.contentFiltration.label')}
|
||||
</Label>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{t('setupPage.forms.emoji.contentFiltration.description')}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
id="content_filtration"
|
||||
checked={config.content_filtration}
|
||||
onCheckedChange={(checked) =>
|
||||
onChange({ ...config, content_filtration: checked })
|
||||
}
|
||||
onCheckedChange={(checked) => onChange({ ...config, content_filtration: checked })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{config.content_filtration && (
|
||||
<div className="space-y-3">
|
||||
<Label htmlFor="filtration_prompt">过滤要求</Label>
|
||||
<Label htmlFor="filtration_prompt">
|
||||
{t('setupPage.forms.emoji.filtrationPrompt.label')}
|
||||
</Label>
|
||||
<Input
|
||||
id="filtration_prompt"
|
||||
placeholder="例如:符合公序良俗"
|
||||
placeholder={t('setupPage.forms.emoji.filtrationPrompt.placeholder')}
|
||||
value={config.filtration_prompt}
|
||||
onChange={(e) =>
|
||||
onChange({ ...config, filtration_prompt: e.target.value })
|
||||
}
|
||||
onChange={(e) => onChange({ ...config, filtration_prompt: e.target.value })}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
描述表情包应该符合的要求
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{t('setupPage.forms.emoji.filtrationPrompt.description')}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
@@ -533,21 +528,21 @@ interface OtherBasicFormProps {
|
||||
}
|
||||
|
||||
export function OtherBasicForm({ config, onChange }: OtherBasicFormProps) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="enable_tool">启用工具系统</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
允许机器人使用各种工具增强功能
|
||||
<Label htmlFor="enable_tool">{t('setupPage.forms.other.enableTool.label')}</Label>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{t('setupPage.forms.other.enableTool.description')}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
id="enable_tool"
|
||||
checked={config.enable_tool}
|
||||
onCheckedChange={(checked) =>
|
||||
onChange({ ...config, enable_tool: checked })
|
||||
}
|
||||
onCheckedChange={(checked) => onChange({ ...config, enable_tool: checked })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -555,17 +550,15 @@ export function OtherBasicForm({ config, onChange }: OtherBasicFormProps) {
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="all_global">启用全局黑话模式</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
允许机器人学习和使用群组黑话
|
||||
<Label htmlFor="all_global">{t('setupPage.forms.other.allGlobal.label')}</Label>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{t('setupPage.forms.other.allGlobal.description')}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
id="all_global"
|
||||
checked={config.all_global}
|
||||
onCheckedChange={(checked) =>
|
||||
onChange({ ...config, all_global: checked })
|
||||
}
|
||||
onCheckedChange={(checked) => onChange({ ...config, all_global: checked })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -579,32 +572,53 @@ interface SiliconFlowFormProps {
|
||||
}
|
||||
|
||||
export function SiliconFlowForm({ config, onChange }: SiliconFlowFormProps) {
|
||||
const { t } = useTranslation()
|
||||
const [showApiKey, setShowApiKey] = useState(false)
|
||||
const apiKeyToggleLabel = showApiKey
|
||||
? t('setupPage.forms.siliconFlow.apiKey.hide')
|
||||
: t('setupPage.forms.siliconFlow.apiKey.show')
|
||||
const autoConfigItems = [
|
||||
t('setupPage.forms.siliconFlow.autoConfig.items.deepseek'),
|
||||
t('setupPage.forms.siliconFlow.autoConfig.items.qwen3'),
|
||||
t('setupPage.forms.siliconFlow.autoConfig.items.qwen3Vl'),
|
||||
t('setupPage.forms.siliconFlow.autoConfig.items.senseVoice'),
|
||||
t('setupPage.forms.siliconFlow.autoConfig.items.bgeM3'),
|
||||
t('setupPage.forms.siliconFlow.autoConfig.items.lpmm'),
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="rounded-lg bg-blue-50 dark:bg-blue-950/30 border border-blue-200 dark:border-blue-800 p-4">
|
||||
<div className="rounded-lg border border-blue-200 bg-blue-50 p-4 dark:border-blue-800 dark:bg-blue-950/30">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="mt-0.5">
|
||||
<svg className="h-5 w-5 text-blue-600 dark:text-blue-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
<svg
|
||||
className="h-5 w-5 text-blue-600 dark:text-blue-400"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="flex-1 text-sm">
|
||||
<p className="font-medium text-blue-900 dark:text-blue-100 mb-1">
|
||||
关于硅基流动 (SiliconFlow)
|
||||
<p className="mb-1 font-medium text-blue-900 dark:text-blue-100">
|
||||
{t('setupPage.forms.siliconFlow.about.title')}
|
||||
</p>
|
||||
<p className="text-blue-700 dark:text-blue-300 mb-2">
|
||||
硅基流动提供了完整的模型覆盖,包括 DeepSeek V3、Qwen、视觉模型、语音识别和嵌入模型。
|
||||
只需一个 API Key 即可使用麦麦的所有功能!
|
||||
<p className="mb-2 text-blue-700 dark:text-blue-300">
|
||||
{t('setupPage.forms.siliconFlow.about.description')}
|
||||
</p>
|
||||
<a
|
||||
href="https://cloud.siliconflow.cn"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-1 text-blue-600 dark:text-blue-400 hover:underline font-medium"
|
||||
className="inline-flex items-center gap-1 font-medium text-blue-600 hover:underline dark:text-blue-400"
|
||||
>
|
||||
前往硅基流动获取 API Key
|
||||
{t('setupPage.forms.siliconFlow.about.link')}
|
||||
<ExternalLink className="h-3 w-3" />
|
||||
</a>
|
||||
</div>
|
||||
@@ -612,7 +626,7 @@ export function SiliconFlowForm({ config, onChange }: SiliconFlowFormProps) {
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<Label htmlFor="siliconflow_api_key">SiliconFlow API Key *</Label>
|
||||
<Label htmlFor="siliconflow_api_key">{t('setupPage.forms.siliconFlow.apiKey.label')}</Label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
id="siliconflow_api_key"
|
||||
@@ -620,46 +634,44 @@ export function SiliconFlowForm({ config, onChange }: SiliconFlowFormProps) {
|
||||
placeholder="sk-..."
|
||||
value={config.api_key}
|
||||
onChange={(e) => onChange({ api_key: e.target.value })}
|
||||
className="font-mono pr-10"
|
||||
className="pr-10 font-mono"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="absolute right-0 top-0 h-full px-3 hover:bg-transparent"
|
||||
className="absolute top-0 right-0 h-full px-3 hover:bg-transparent"
|
||||
onClick={() => setShowApiKey(!showApiKey)}
|
||||
aria-label={apiKeyToggleLabel}
|
||||
title={apiKeyToggleLabel}
|
||||
>
|
||||
{showApiKey ? (
|
||||
<EyeOff className="h-4 w-4 text-muted-foreground" />
|
||||
<EyeOff className="text-muted-foreground h-4 w-4" />
|
||||
) : (
|
||||
<Eye className="h-4 w-4 text-muted-foreground" />
|
||||
<Eye className="text-muted-foreground h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
请输入您的硅基流动 API 密钥。获取后,麦麦将自动配置所有必需的模型。
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{t('setupPage.forms.siliconFlow.apiKey.description')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg bg-muted/50 p-4 text-sm space-y-2">
|
||||
<p className="font-medium">将自动配置以下模型:</p>
|
||||
<ul className="list-disc list-inside space-y-1 text-muted-foreground ml-2">
|
||||
<li>DeepSeek V3 - 主要对话和工具模型</li>
|
||||
<li>Qwen3 30B - 高频小任务和工具调用</li>
|
||||
<li>Qwen3 VL 30B - 图像识别</li>
|
||||
<li>SenseVoice - 语音识别</li>
|
||||
<li>BGE-M3 - 文本嵌入</li>
|
||||
<li>知识库相关模型 (LPMM)</li>
|
||||
<div className="bg-muted/50 space-y-2 rounded-lg p-4 text-sm">
|
||||
<p className="font-medium">{t('setupPage.forms.siliconFlow.autoConfig.title')}</p>
|
||||
<ul className="text-muted-foreground ml-2 list-inside list-disc space-y-1">
|
||||
{autoConfigItems.map((item) => (
|
||||
<li key={item}>{item}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border border-amber-200 dark:border-amber-800 bg-amber-50 dark:bg-amber-950/30 p-4">
|
||||
<div className="rounded-lg border border-amber-200 bg-amber-50 p-4 dark:border-amber-800 dark:bg-amber-950/30">
|
||||
<p className="text-sm text-amber-900 dark:text-amber-100">
|
||||
<span className="font-medium">💡 提示:</span>
|
||||
完成向导后,您可以在"系统设置 → 模型配置"中添加更多 API 提供商和模型。
|
||||
<span className="font-medium">{t('setupPage.forms.siliconFlow.hint.title')}</span>
|
||||
{t('setupPage.forms.siliconFlow.hint.description')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useNavigate } from '@tanstack/react-router'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
Sparkles,
|
||||
ArrowRight,
|
||||
CheckCircle2,
|
||||
SkipForward,
|
||||
Bot,
|
||||
User,
|
||||
Smile,
|
||||
Settings,
|
||||
Key,
|
||||
CheckCircle2,
|
||||
Globe,
|
||||
Key,
|
||||
Settings,
|
||||
SkipForward,
|
||||
Smile,
|
||||
Sparkles,
|
||||
User,
|
||||
} from 'lucide-react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent } from '@/components/ui/card'
|
||||
import { Progress } from '@/components/ui/progress'
|
||||
@@ -69,7 +69,7 @@ import { RestartProvider, useRestart } from '@/lib/restart-context'
|
||||
import { RestartOverlay } from '@/components/restart-overlay'
|
||||
|
||||
const LANGUAGE_CODES = ['zh', 'en', 'ja', 'ko'] as const
|
||||
const LANGUAGE_NAMES: Record<typeof LANGUAGE_CODES[number], string> = {
|
||||
const LANGUAGE_NAMES: Record<(typeof LANGUAGE_CODES)[number], string> = {
|
||||
zh: '中文',
|
||||
en: 'English',
|
||||
ja: '日本語',
|
||||
@@ -88,10 +88,26 @@ export function SetupPage() {
|
||||
// 内部实现组件
|
||||
function SetupPageContent() {
|
||||
const navigate = useNavigate()
|
||||
const { t, i18n: i18nInstance } = useTranslation()
|
||||
const { toast } = useToast()
|
||||
const { triggerRestart } = useRestart()
|
||||
const { i18n: i18nInstance } = useTranslation()
|
||||
const currentLang = i18nInstance.language || 'zh'
|
||||
const currentLang = i18nInstance.resolvedLanguage || i18nInstance.language || 'zh'
|
||||
const createDefaultPersonalityConfig = (): PersonalityConfig => ({
|
||||
personality: t('setupPage.defaults.personality.personality'),
|
||||
reply_style: t('setupPage.defaults.personality.replyStyle'),
|
||||
interest: t('setupPage.defaults.personality.interest'),
|
||||
plan_style: t('setupPage.defaults.personality.planStyle'),
|
||||
private_plan_style: t('setupPage.defaults.personality.privatePlanStyle'),
|
||||
})
|
||||
const createDefaultEmojiConfig = (): EmojiConfig => ({
|
||||
emoji_chance: 0.4,
|
||||
max_reg_num: 40,
|
||||
do_replace: true,
|
||||
check_interval: 10,
|
||||
steal_emoji: true,
|
||||
content_filtration: false,
|
||||
filtration_prompt: t('setupPage.defaults.emoji.filtrationPrompt'),
|
||||
})
|
||||
const [currentStep, setCurrentStep] = useState(0)
|
||||
const [isCompleting, setIsCompleting] = useState(false)
|
||||
const [isSaving, setIsSaving] = useState(false)
|
||||
@@ -107,28 +123,12 @@ function SetupPageContent() {
|
||||
})
|
||||
|
||||
// 步骤2:人格配置
|
||||
const [personality, setPersonality] = useState<PersonalityConfig>({
|
||||
personality: '是一个女大学生,现在在读大二,会刷贴吧。',
|
||||
reply_style:
|
||||
'请回复的平淡一些,简短一些,说中文,不要刻意突出自身学科背景。可以参考贴吧,知乎和微博的回复风格。',
|
||||
interest:
|
||||
'对技术相关话题,游戏和动漫相关话题感兴趣,也对日常话题感兴趣,不喜欢太过沉重严肃的话题',
|
||||
plan_style:
|
||||
'1.思考**所有**的可用的action中的**每个动作**是否符合当下条件,如果动作使用条件符合聊天内容就使用\n2.如果相同的内容已经被执行,请不要重复执行\n3.请控制你的发言频率,不要太过频繁的发言\n4.如果有人对你感到厌烦,请减少回复\n5.如果有人对你进行攻击,或者情绪激动,请你以合适的方法应对',
|
||||
private_plan_style:
|
||||
'1.思考**所有**的可用的action中的**每个动作**是否符合当下条件,如果动作使用条件符合聊天内容就使用\n2.如果相同的内容已经被执行,请不要重复执行\n3.某句话如果已经被回复过,不要重复回复',
|
||||
})
|
||||
const [personality, setPersonality] = useState<PersonalityConfig>(() =>
|
||||
createDefaultPersonalityConfig()
|
||||
)
|
||||
|
||||
// 步骤3:表情包配置
|
||||
const [emoji, setEmoji] = useState<EmojiConfig>({
|
||||
emoji_chance: 0.4,
|
||||
max_reg_num: 40,
|
||||
do_replace: true,
|
||||
check_interval: 10,
|
||||
steal_emoji: true,
|
||||
content_filtration: false,
|
||||
filtration_prompt: '符合公序良俗',
|
||||
})
|
||||
const [emoji, setEmoji] = useState<EmojiConfig>(() => createDefaultEmojiConfig())
|
||||
|
||||
// 步骤4:其他基础配置
|
||||
const [otherBasic, setOtherBasic] = useState<OtherBasicConfig>({
|
||||
@@ -144,32 +144,32 @@ function SetupPageContent() {
|
||||
const steps: SetupStep[] = [
|
||||
{
|
||||
id: 'bot-basic',
|
||||
title: 'Bot基础',
|
||||
description: '配置机器人的基本信息',
|
||||
title: t('setupPage.steps.botBasic.title'),
|
||||
description: t('setupPage.steps.botBasic.description'),
|
||||
icon: Bot,
|
||||
},
|
||||
{
|
||||
id: 'personality',
|
||||
title: '人格配置',
|
||||
description: '定义机器人的性格和说话风格',
|
||||
title: t('setupPage.steps.personality.title'),
|
||||
description: t('setupPage.steps.personality.description'),
|
||||
icon: User,
|
||||
},
|
||||
{
|
||||
id: 'emoji',
|
||||
title: '表情包',
|
||||
description: '配置表情包相关设置',
|
||||
title: t('setupPage.steps.emoji.title'),
|
||||
description: t('setupPage.steps.emoji.description'),
|
||||
icon: Smile,
|
||||
},
|
||||
{
|
||||
id: 'other',
|
||||
title: '其他设置',
|
||||
description: '工具、情绪系统等配置',
|
||||
title: t('setupPage.steps.other.title'),
|
||||
description: t('setupPage.steps.other.description'),
|
||||
icon: Settings,
|
||||
},
|
||||
{
|
||||
id: 'siliconflow',
|
||||
title: 'API配置',
|
||||
description: '配置硅基流动API密钥',
|
||||
title: t('setupPage.steps.siliconFlow.title'),
|
||||
description: t('setupPage.steps.siliconFlow.description'),
|
||||
icon: Key,
|
||||
},
|
||||
]
|
||||
@@ -198,11 +198,9 @@ function SetupPageContent() {
|
||||
setSiliconFlow(silicon)
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: '加载配置失败',
|
||||
title: t('setupPage.toast.loadFailedTitle'),
|
||||
description:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: '无法加载现有配置,将使用默认值',
|
||||
error instanceof Error ? error.message : t('setupPage.toast.loadFailedDescription'),
|
||||
variant: 'destructive',
|
||||
})
|
||||
} finally {
|
||||
@@ -211,7 +209,7 @@ function SetupPageContent() {
|
||||
}
|
||||
|
||||
loadConfigs()
|
||||
}, [toast])
|
||||
}, [t, toast])
|
||||
|
||||
// 保存当前步骤配置
|
||||
const saveCurrentStep = async () => {
|
||||
@@ -236,14 +234,16 @@ function SetupPageContent() {
|
||||
}
|
||||
|
||||
toast({
|
||||
title: '保存成功',
|
||||
description: `${steps[currentStep].title}配置已保存`,
|
||||
title: t('setupPage.toast.saveSuccessTitle'),
|
||||
description: t('setupPage.toast.saveSuccessDescription', {
|
||||
step: steps[currentStep].title,
|
||||
}),
|
||||
})
|
||||
return true
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: '保存失败',
|
||||
description: error instanceof Error ? error.message : '未知错误',
|
||||
title: t('setupPage.toast.saveFailedTitle'),
|
||||
description: error instanceof Error ? error.message : t('setupPage.toast.unknownError'),
|
||||
variant: 'destructive',
|
||||
})
|
||||
return false
|
||||
@@ -254,15 +254,17 @@ function SetupPageContent() {
|
||||
|
||||
// Step 1 验证
|
||||
function validateBotBasic(config: BotBasicConfig): string | null {
|
||||
if (!config.platform) return '请选择平台'
|
||||
if (!config.nickname.trim()) return '请输入昵称'
|
||||
if (!config.platform) return t('setupPage.validation.selectPlatform')
|
||||
if (!config.nickname.trim()) return t('setupPage.validation.enterNickname')
|
||||
if (config.platform === 'qq') {
|
||||
if (!config.qq_account || config.qq_account <= 0) return '请输入QQ账号'
|
||||
if (!config.qq_account || config.qq_account <= 0) {
|
||||
return t('setupPage.validation.enterQqAccount')
|
||||
}
|
||||
} else {
|
||||
const hasAccount = config.platforms.some(
|
||||
(p) => p.startsWith(config.platform + ':') && p.split(':')[1]?.trim()
|
||||
)
|
||||
if (!hasAccount) return '请输入账号ID'
|
||||
if (!hasAccount) return t('setupPage.validation.enterAccountId')
|
||||
}
|
||||
return null
|
||||
}
|
||||
@@ -272,7 +274,11 @@ function SetupPageContent() {
|
||||
if (currentStep === 0) {
|
||||
const error = validateBotBasic(botBasic)
|
||||
if (error) {
|
||||
toast({ title: '验证失败', description: error, variant: 'destructive' })
|
||||
toast({
|
||||
title: t('setupPage.toast.validationFailedTitle'),
|
||||
description: error,
|
||||
variant: 'destructive',
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -308,16 +314,18 @@ function SetupPageContent() {
|
||||
await completeSetup()
|
||||
|
||||
toast({
|
||||
title: '配置完成',
|
||||
description: '麦麦正在重启以应用新配置...',
|
||||
title: t('setupPage.toast.completeSuccessTitle'),
|
||||
description: t('setupPage.toast.completeSuccessDescription', {
|
||||
appName: APP_NAME,
|
||||
}),
|
||||
})
|
||||
|
||||
// 3. 触发麦麦重启(使用新的重启组件)
|
||||
await triggerRestart()
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: '配置失败',
|
||||
description: error instanceof Error ? error.message : '未知错误',
|
||||
title: t('setupPage.toast.completeFailedTitle'),
|
||||
description: error instanceof Error ? error.message : t('setupPage.toast.unknownError'),
|
||||
variant: 'destructive',
|
||||
})
|
||||
} finally {
|
||||
@@ -331,8 +339,8 @@ function SetupPageContent() {
|
||||
navigate({ to: '/' })
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: '跳过失败',
|
||||
description: error instanceof Error ? error.message : '未知错误',
|
||||
title: t('setupPage.toast.skipFailedTitle'),
|
||||
description: error instanceof Error ? error.message : t('setupPage.toast.unknownError'),
|
||||
variant: 'destructive',
|
||||
})
|
||||
}
|
||||
@@ -344,9 +352,7 @@ function SetupPageContent() {
|
||||
case 0:
|
||||
return <BotBasicForm config={botBasic} onChange={setBotBasic} />
|
||||
case 1:
|
||||
return (
|
||||
<PersonalityForm config={personality} onChange={setPersonality} />
|
||||
)
|
||||
return <PersonalityForm config={personality} onChange={setPersonality} />
|
||||
case 2:
|
||||
return <EmojiForm config={emoji} onChange={setEmoji} />
|
||||
case 3:
|
||||
@@ -359,18 +365,19 @@ function SetupPageContent() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative flex min-h-screen flex-col items-center justify-center overflow-hidden bg-gradient-to-br from-primary/5 via-background to-secondary/5 p-4 md:p-6">
|
||||
<div className="from-primary/5 via-background to-secondary/5 relative flex min-h-screen flex-col items-center justify-center overflow-hidden bg-gradient-to-br p-4 md:p-6">
|
||||
{/* 重启遮罩层 */}
|
||||
<RestartOverlay />
|
||||
|
||||
{/* 语言切换 */}
|
||||
<div className="absolute right-4 top-4 z-20">
|
||||
<div className="absolute top-4 right-4 z-20">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="sm" className="gap-2">
|
||||
<Globe className="h-4 w-4" />
|
||||
<span className="hidden sm:inline text-xs">
|
||||
{LANGUAGE_NAMES[currentLang.split('-')[0] as typeof LANGUAGE_CODES[number]] ?? currentLang}
|
||||
<span className="hidden text-xs sm:inline">
|
||||
{LANGUAGE_NAMES[currentLang.split('-')[0] as (typeof LANGUAGE_CODES)[number]] ??
|
||||
currentLang}
|
||||
</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
@@ -381,12 +388,10 @@ function SetupPageContent() {
|
||||
onClick={() => i18nInstance.changeLanguage(code)}
|
||||
className={cn(
|
||||
'cursor-pointer',
|
||||
currentLang.split('-')[0] === code && 'font-semibold text-primary'
|
||||
currentLang.split('-')[0] === code && 'text-primary font-semibold'
|
||||
)}
|
||||
>
|
||||
{currentLang.split('-')[0] === code && (
|
||||
<span className="mr-2">✓</span>
|
||||
)}
|
||||
{currentLang.split('-')[0] === code && <span className="mr-2">✓</span>}
|
||||
{LANGUAGE_NAMES[code]}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
@@ -395,232 +400,217 @@ function SetupPageContent() {
|
||||
</div>
|
||||
|
||||
{/* 背景装饰 */}
|
||||
<div className="absolute inset-0 overflow-hidden pointer-events-none">
|
||||
<div className="absolute left-1/4 top-1/4 h-64 w-64 md:h-96 md:w-96 rounded-full bg-primary/5 blur-3xl" />
|
||||
<div className="absolute right-1/4 bottom-1/4 h-64 w-64 md:h-96 md:w-96 rounded-full bg-secondary/5 blur-3xl" />
|
||||
<div className="pointer-events-none absolute inset-0 overflow-hidden">
|
||||
<div className="bg-primary/5 absolute top-1/4 left-1/4 h-64 w-64 rounded-full blur-3xl md:h-96 md:w-96" />
|
||||
<div className="bg-secondary/5 absolute right-1/4 bottom-1/4 h-64 w-64 rounded-full blur-3xl md:h-96 md:w-96" />
|
||||
</div>
|
||||
|
||||
{/* 加载状态 */}
|
||||
{isLoading ? (
|
||||
<div className="relative z-10 text-center">
|
||||
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center">
|
||||
<div className="h-12 w-12 animate-spin rounded-full border-4 border-primary border-t-transparent" />
|
||||
<div className="border-primary h-12 w-12 animate-spin rounded-full border-4 border-t-transparent" />
|
||||
</div>
|
||||
<p className="text-lg font-medium">加载配置中...</p>
|
||||
<p className="text-sm text-muted-foreground mt-2">
|
||||
正在读取现有配置
|
||||
</p>
|
||||
<p className="text-lg font-medium">{t('setupPage.loading.title')}</p>
|
||||
<p className="text-muted-foreground mt-2 text-sm">{t('setupPage.loading.description')}</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* 主要内容 */}
|
||||
<div className="relative z-10 w-full max-w-4xl">
|
||||
{/* 头部 */}
|
||||
<div className="mb-6 md:mb-8 text-center">
|
||||
<div className="mx-auto mb-4 flex h-12 w-12 md:h-16 md:w-16 items-center justify-center rounded-2xl bg-primary/10">
|
||||
<Sparkles
|
||||
className="h-6 w-6 md:h-8 md:w-8 text-primary"
|
||||
strokeWidth={2}
|
||||
fill="none"
|
||||
/>
|
||||
</div>
|
||||
<h1 className="mb-2 text-2xl md:text-3xl font-bold">
|
||||
首次配置向导
|
||||
</h1>
|
||||
<p className="text-sm md:text-base text-muted-foreground">
|
||||
让我们一起完成 {APP_NAME} 的初始配置
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 进度条 */}
|
||||
<div className="mb-6 md:mb-8">
|
||||
<div className="mb-2 flex items-center justify-between text-xs md:text-sm">
|
||||
<span className="text-muted-foreground">
|
||||
步骤 {currentStep + 1} / {steps.length}
|
||||
</span>
|
||||
<span className="font-medium text-primary">
|
||||
{Math.round(progress)}%
|
||||
</span>
|
||||
</div>
|
||||
<Progress value={progress} className="h-2" />
|
||||
</div>
|
||||
|
||||
{/* 步骤指示器 */}
|
||||
<div className="mb-6 md:mb-8 flex justify-between">
|
||||
{steps.map((step, index) => {
|
||||
const Icon = step.icon
|
||||
return (
|
||||
<div
|
||||
key={step.id}
|
||||
className={cn(
|
||||
'flex flex-1 flex-col items-center gap-1 md:gap-2',
|
||||
index < steps.length - 1 && 'relative'
|
||||
)}
|
||||
>
|
||||
{/* 连接线 */}
|
||||
{index < steps.length - 1 && (
|
||||
<div
|
||||
className={cn(
|
||||
'absolute left-1/2 top-3 md:top-4 h-0.5 w-full',
|
||||
index < currentStep ? 'bg-primary' : 'bg-border'
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 步骤圆圈 */}
|
||||
<div
|
||||
className={cn(
|
||||
'relative z-10 flex h-6 w-6 md:h-8 md:w-8 items-center justify-center rounded-full border-2 transition-all',
|
||||
index === currentStep
|
||||
? 'border-primary bg-primary text-primary-foreground'
|
||||
: index < currentStep
|
||||
? 'border-primary bg-primary text-primary-foreground'
|
||||
: 'border-border bg-background text-muted-foreground'
|
||||
)}
|
||||
>
|
||||
{index < currentStep ? (
|
||||
<CheckCircle2
|
||||
className="h-3 w-3 md:h-4 md:w-4"
|
||||
strokeWidth={2.5}
|
||||
fill="none"
|
||||
/>
|
||||
) : (
|
||||
<Icon className="h-3 w-3 md:h-4 md:w-4" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 步骤标题 */}
|
||||
<span
|
||||
className={cn(
|
||||
'text-[10px] md:text-xs text-center max-w-[60px] md:max-w-none truncate md:whitespace-normal',
|
||||
index === currentStep
|
||||
? 'font-medium text-foreground'
|
||||
: 'text-muted-foreground'
|
||||
)}
|
||||
title={step.title}
|
||||
>
|
||||
{step.title}
|
||||
</span>
|
||||
{/* 头部 */}
|
||||
<div className="mb-6 text-center md:mb-8">
|
||||
<div className="bg-primary/10 mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-2xl md:h-16 md:w-16">
|
||||
<Sparkles
|
||||
className="text-primary h-6 w-6 md:h-8 md:w-8"
|
||||
strokeWidth={2}
|
||||
fill="none"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* 步骤内容卡片 */}
|
||||
<Card className="mb-6 md:mb-8 shadow-lg">
|
||||
<CardContent className="p-4 md:p-8">
|
||||
<div className="min-h-[300px] md:min-h-[400px]">
|
||||
<div className="mb-4 md:mb-6">
|
||||
<h2 className="mb-2 text-xl md:text-2xl font-semibold">
|
||||
{steps[currentStep].title}
|
||||
</h2>
|
||||
<p className="text-sm md:text-base text-muted-foreground">
|
||||
{steps[currentStep].description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 表单内容 */}
|
||||
<ScrollArea className="h-[400px] md:h-[500px]">
|
||||
<div className="pr-2">
|
||||
{renderStepForm()}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
<h1 className="mb-2 text-2xl font-bold md:text-3xl">{t('setupPage.header.title')}</h1>
|
||||
<p className="text-muted-foreground text-sm md:text-base">
|
||||
{t('setupPage.header.description', { appName: APP_NAME })}
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<div className="flex flex-col sm:flex-row items-center justify-between gap-3 sm:gap-0">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handlePrevious}
|
||||
disabled={currentStep === 0 || isSaving}
|
||||
className="w-full sm:w-auto order-2 sm:order-1"
|
||||
>
|
||||
上一步
|
||||
</Button>
|
||||
{/* 进度条 */}
|
||||
<div className="mb-6 md:mb-8">
|
||||
<div className="mb-2 flex items-center justify-between text-xs md:text-sm">
|
||||
<span className="text-muted-foreground">
|
||||
{t('setupPage.progress.stepCounter', {
|
||||
current: currentStep + 1,
|
||||
total: steps.length,
|
||||
})}
|
||||
</span>
|
||||
<span className="text-primary font-medium">{Math.round(progress)}%</span>
|
||||
</div>
|
||||
<Progress value={progress} className="h-2" />
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 w-full sm:w-auto order-1 sm:order-2">
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="flex-1 sm:flex-none gap-2"
|
||||
disabled={isSaving || isCompleting}
|
||||
>
|
||||
<SkipForward className="h-4 w-4" strokeWidth={2} fill="none" />
|
||||
跳过向导
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>确认跳过配置向导</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
您可以随时在系统设置中重新进入配置向导。确定要跳过吗?
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>取消</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handleSkip}>
|
||||
确认跳过
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
{/* 步骤指示器 */}
|
||||
<div className="mb-6 flex justify-between md:mb-8">
|
||||
{steps.map((step, index) => {
|
||||
const Icon = step.icon
|
||||
return (
|
||||
<div
|
||||
key={step.id}
|
||||
className={cn(
|
||||
'flex flex-1 flex-col items-center gap-1 md:gap-2',
|
||||
index < steps.length - 1 && 'relative'
|
||||
)}
|
||||
>
|
||||
{/* 连接线 */}
|
||||
{index < steps.length - 1 && (
|
||||
<div
|
||||
className={cn(
|
||||
'absolute top-3 left-1/2 h-0.5 w-full md:top-4',
|
||||
index < currentStep ? 'bg-primary' : 'bg-border'
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{currentStep === steps.length - 1 ? (
|
||||
{/* 步骤圆圈 */}
|
||||
<div
|
||||
className={cn(
|
||||
'relative z-10 flex h-6 w-6 items-center justify-center rounded-full border-2 transition-all md:h-8 md:w-8',
|
||||
index === currentStep
|
||||
? 'border-primary bg-primary text-primary-foreground'
|
||||
: index < currentStep
|
||||
? 'border-primary bg-primary text-primary-foreground'
|
||||
: 'border-border bg-background text-muted-foreground'
|
||||
)}
|
||||
>
|
||||
{index < currentStep ? (
|
||||
<CheckCircle2
|
||||
className="h-3 w-3 md:h-4 md:w-4"
|
||||
strokeWidth={2.5}
|
||||
fill="none"
|
||||
/>
|
||||
) : (
|
||||
<Icon className="h-3 w-3 md:h-4 md:w-4" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 步骤标题 */}
|
||||
<span
|
||||
className={cn(
|
||||
'max-w-[60px] truncate text-center text-[10px] md:max-w-none md:text-xs md:whitespace-normal',
|
||||
index === currentStep
|
||||
? 'text-foreground font-medium'
|
||||
: 'text-muted-foreground'
|
||||
)}
|
||||
title={step.title}
|
||||
>
|
||||
{step.title}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* 步骤内容卡片 */}
|
||||
<Card className="mb-6 shadow-lg md:mb-8">
|
||||
<CardContent className="p-4 md:p-8">
|
||||
<div className="min-h-[300px] md:min-h-[400px]">
|
||||
<div className="mb-4 md:mb-6">
|
||||
<h2 className="mb-2 text-xl font-semibold md:text-2xl">
|
||||
{steps[currentStep].title}
|
||||
</h2>
|
||||
<p className="text-muted-foreground text-sm md:text-base">
|
||||
{steps[currentStep].description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 表单内容 */}
|
||||
<ScrollArea className="h-[400px] md:h-[500px]">
|
||||
<div className="pr-2">{renderStepForm()}</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<div className="flex flex-col items-center justify-between gap-3 sm:flex-row sm:gap-0">
|
||||
<Button
|
||||
onClick={handleComplete}
|
||||
disabled={isCompleting || isSaving}
|
||||
className="flex-1 sm:flex-none"
|
||||
variant="outline"
|
||||
onClick={handlePrevious}
|
||||
disabled={currentStep === 0 || isSaving}
|
||||
className="order-2 w-full sm:order-1 sm:w-auto"
|
||||
>
|
||||
{isCompleting || isSaving ? (
|
||||
<>
|
||||
<div className="mr-2 h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent" />
|
||||
{isSaving ? '保存中...' : '完成中...'}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
完成配置
|
||||
<CheckCircle2
|
||||
className="ml-2 h-4 w-4"
|
||||
strokeWidth={2}
|
||||
fill="none"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{t('setupPage.actions.previous')}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
onClick={handleNext}
|
||||
disabled={isSaving}
|
||||
className="flex-1 sm:flex-none"
|
||||
>
|
||||
{isSaving ? (
|
||||
<>
|
||||
<div className="mr-2 h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent" />
|
||||
保存中...
|
||||
</>
|
||||
|
||||
<div className="order-1 flex w-full gap-2 sm:order-2 sm:w-auto">
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="flex-1 gap-2 sm:flex-none"
|
||||
disabled={isSaving || isCompleting}
|
||||
>
|
||||
<SkipForward className="h-4 w-4" strokeWidth={2} fill="none" />
|
||||
{t('setupPage.actions.skip')}
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>{t('setupPage.skipDialog.title')}</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
{t('setupPage.skipDialog.description')}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>{t('common.cancel')}</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handleSkip}>
|
||||
{t('setupPage.skipDialog.confirm')}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
{currentStep === steps.length - 1 ? (
|
||||
<Button
|
||||
onClick={handleComplete}
|
||||
disabled={isCompleting || isSaving}
|
||||
className="flex-1 sm:flex-none"
|
||||
>
|
||||
{isCompleting || isSaving ? (
|
||||
<>
|
||||
<div className="mr-2 h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent" />
|
||||
{isSaving
|
||||
? t('setupPage.actions.saving')
|
||||
: t('setupPage.actions.completing')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{t('setupPage.actions.complete')}
|
||||
<CheckCircle2 className="ml-2 h-4 w-4" strokeWidth={2} fill="none" />
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
) : (
|
||||
<>
|
||||
下一步
|
||||
<ArrowRight
|
||||
className="ml-2 h-4 w-4"
|
||||
strokeWidth={2}
|
||||
fill="none"
|
||||
/>
|
||||
</>
|
||||
<Button onClick={handleNext} disabled={isSaving} className="flex-1 sm:flex-none">
|
||||
{isSaving ? (
|
||||
<>
|
||||
<div className="mr-2 h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent" />
|
||||
{t('setupPage.actions.saving')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{t('setupPage.actions.next')}
|
||||
<ArrowRight className="ml-2 h-4 w-4" strokeWidth={2} fill="none" />
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 页脚提示 */}
|
||||
<div className="relative z-10 mt-6 md:mt-8 text-center text-xs text-muted-foreground">
|
||||
<p>您可以随时在设置中修改这些配置</p>
|
||||
</div>
|
||||
{/* 页脚提示 */}
|
||||
<div className="text-muted-foreground relative z-10 mt-6 text-center text-xs md:mt-8">
|
||||
<p>{t('setupPage.footer')}</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user