2
0
Эх сурвалжийг харах

Adds refresh models button for Unbound provider (#3663)

* Adds refresh models button for Unbound provider

* Adds changeset

* Optimizes code to prevent memory leak, add error messages

* Adds unbound messages to all supported languages

---------

Co-authored-by: Pugazhendhi <[email protected]>
pugazhendhi-m 7 сар өмнө
parent
commit
d851586042

+ 10 - 0
.changeset/seven-kids-return.md

@@ -0,0 +1,10 @@
+---
+"roo-cline": minor
+---
+
+Adds refresh models button for Unbound provider
+Adds a button above model picker to refresh models based on the current API Key.
+
+1. Clicking the refresh button saves the API Key and calls /models endpoint using that.
+2. Gets the new models and updates the current model if it is invalid for the given API Key.
+3. The refresh button also flushes existing Unbound models and refetches them.

+ 2 - 1
src/api/providers/fetchers/modelCache.ts

@@ -65,7 +65,8 @@ export const getModels = async (
 			models = await getGlamaModels()
 			break
 		case "unbound":
-			models = await getUnboundModels()
+			// Unbound models endpoint requires an API key to fetch application specific models
+			models = await getUnboundModels(apiKey)
 			break
 		case "litellm":
 			if (apiKey && baseUrl) {

+ 9 - 2
src/api/providers/fetchers/unbound.ts

@@ -2,11 +2,17 @@ import axios from "axios"
 
 import { ModelInfo } from "../../../shared/api"
 
-export async function getUnboundModels(): Promise<Record<string, ModelInfo>> {
+export async function getUnboundModels(apiKey?: string | null): Promise<Record<string, ModelInfo>> {
 	const models: Record<string, ModelInfo> = {}
 
 	try {
-		const response = await axios.get("https://api.getunbound.ai/models")
+		const headers: Record<string, string> = {}
+
+		if (apiKey) {
+			headers["Authorization"] = `Bearer ${apiKey}`
+		}
+
+		const response = await axios.get("https://api.getunbound.ai/models", { headers })
 
 		if (response.data) {
 			const rawModels: Record<string, any> = response.data
@@ -40,6 +46,7 @@ export async function getUnboundModels(): Promise<Record<string, ModelInfo>> {
 		}
 	} catch (error) {
 		console.error(`Error fetching Unbound models: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`)
+		throw new Error(`Failed to fetch Unbound models: ${error instanceof Error ? error.message : "Unknown error"}`)
 	}
 
 	return models

+ 113 - 1
webview-ui/src/components/settings/providers/Unbound.tsx

@@ -1,10 +1,13 @@
-import { useCallback } from "react"
+import { useCallback, useState, useRef } from "react"
 import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
+import { useQueryClient } from "@tanstack/react-query"
 
 import { ProviderSettings, RouterModels, unboundDefaultModelId } from "@roo/shared/api"
 
 import { useAppTranslation } from "@src/i18n/TranslationContext"
 import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink"
+import { vscode } from "@src/utils/vscode"
+import { Button } from "@src/components/ui"
 
 import { inputEventTransform } from "../transforms"
 import { ModelPicker } from "../ModelPicker"
@@ -17,6 +20,13 @@ type UnboundProps = {
 
 export const Unbound = ({ apiConfiguration, setApiConfigurationField, routerModels }: UnboundProps) => {
 	const { t } = useAppTranslation()
+	const [didRefetch, setDidRefetch] = useState<boolean>()
+	const [isInvalidKey, setIsInvalidKey] = useState<boolean>(false)
+	const queryClient = useQueryClient()
+
+	// Add refs to store timer IDs
+	const didRefetchTimerRef = useRef<NodeJS.Timeout>()
+	const invalidKeyTimerRef = useRef<NodeJS.Timeout>()
 
 	const handleInputChange = useCallback(
 		<K extends keyof ProviderSettings, E>(
@@ -29,6 +39,90 @@ export const Unbound = ({ apiConfiguration, setApiConfigurationField, routerMode
 		[setApiConfigurationField],
 	)
 
+	const saveConfiguration = useCallback(async () => {
+		vscode.postMessage({
+			type: "upsertApiConfiguration",
+			text: "default",
+			apiConfiguration: apiConfiguration,
+		})
+
+		const waitForStateUpdate = new Promise<void>((resolve, reject) => {
+			const timeoutId = setTimeout(() => {
+				window.removeEventListener("message", messageHandler)
+				reject(new Error("Timeout waiting for state update"))
+			}, 10000) // 10 second timeout
+
+			const messageHandler = (event: MessageEvent) => {
+				const message = event.data
+				if (message.type === "state") {
+					clearTimeout(timeoutId)
+					window.removeEventListener("message", messageHandler)
+					resolve()
+				}
+			}
+			window.addEventListener("message", messageHandler)
+		})
+
+		try {
+			await waitForStateUpdate
+		} catch (error) {
+			console.error("Failed to save configuration:", error)
+		}
+	}, [apiConfiguration])
+
+	const requestModels = useCallback(async () => {
+		vscode.postMessage({ type: "flushRouterModels", text: "unbound" })
+
+		const modelsPromise = new Promise<void>((resolve) => {
+			const messageHandler = (event: MessageEvent) => {
+				const message = event.data
+				if (message.type === "routerModels") {
+					window.removeEventListener("message", messageHandler)
+					resolve()
+				}
+			}
+			window.addEventListener("message", messageHandler)
+		})
+
+		vscode.postMessage({ type: "requestRouterModels" })
+
+		await modelsPromise
+
+		await queryClient.invalidateQueries({ queryKey: ["routerModels"] })
+
+		// After refreshing models, check if current model is in the updated list
+		// If not, select the first available model
+		const updatedModels = queryClient.getQueryData<{ unbound: RouterModels }>(["routerModels"])?.unbound
+		if (updatedModels && Object.keys(updatedModels).length > 0) {
+			const currentModelId = apiConfiguration?.unboundModelId
+			const modelExists = currentModelId && Object.prototype.hasOwnProperty.call(updatedModels, currentModelId)
+
+			if (!currentModelId || !modelExists) {
+				const firstAvailableModelId = Object.keys(updatedModels)[0]
+				setApiConfigurationField("unboundModelId", firstAvailableModelId)
+			}
+		}
+
+		if (!updatedModels || Object.keys(updatedModels).includes("error")) {
+			return false
+		} else {
+			return true
+		}
+	}, [queryClient, apiConfiguration, setApiConfigurationField])
+
+	const handleRefresh = useCallback(async () => {
+		await saveConfiguration()
+		const requestModelsResult = await requestModels()
+
+		if (requestModelsResult) {
+			setDidRefetch(true)
+			didRefetchTimerRef.current = setTimeout(() => setDidRefetch(false), 3000)
+		} else {
+			setIsInvalidKey(true)
+			invalidKeyTimerRef.current = setTimeout(() => setIsInvalidKey(false), 3000)
+		}
+	}, [saveConfiguration, requestModels])
+
 	return (
 		<>
 			<VSCodeTextField
@@ -47,6 +141,24 @@ export const Unbound = ({ apiConfiguration, setApiConfigurationField, routerMode
 					{t("settings:providers.getUnboundApiKey")}
 				</VSCodeButtonLink>
 			)}
+			<div className="flex justify-end">
+				<Button variant="outline" onClick={handleRefresh} className="w-1/2 max-w-xs">
+					<div className="flex items-center gap-2 justify-center">
+						<span className="codicon codicon-refresh" />
+						{t("settings:providers.refreshModels.label")}
+					</div>
+				</Button>
+			</div>
+			{didRefetch && (
+				<div className="flex items-center text-vscode-charts-green">
+					{t("settings:providers.unboundRefreshModelsSuccess")}
+				</div>
+			)}
+			{isInvalidKey && (
+				<div className="flex items-center text-vscode-errorForeground">
+					{t("settings:providers.unboundInvalidApiKey")}
+				</div>
+			)}
 			<ModelPicker
 				apiConfiguration={apiConfiguration}
 				defaultModelId={unboundDefaultModelId}

+ 2 - 0
webview-ui/src/i18n/locales/ca/settings.json

@@ -185,6 +185,8 @@
 		},
 		"unboundApiKey": "Clau API d'Unbound",
 		"getUnboundApiKey": "Obtenir clau API d'Unbound",
+		"unboundRefreshModelsSuccess": "Llista de models actualitzada! Ara podeu seleccionar entre els últims models.",
+		"unboundInvalidApiKey": "Clau API no vàlida. Si us plau, comproveu la vostra clau API i torneu-ho a provar.",
 		"humanRelay": {
 			"description": "No es requereix clau API, però l'usuari necessita ajuda per copiar i enganxar informació al xat d'IA web.",
 			"instructions": "Durant l'ús, apareixerà un diàleg i el missatge actual es copiarà automàticament al porta-retalls. Necessiteu enganxar-lo a les versions web d'IA (com ChatGPT o Claude), després copiar la resposta de l'IA de nou al diàleg i fer clic al botó de confirmació."

+ 2 - 0
webview-ui/src/i18n/locales/de/settings.json

@@ -185,6 +185,8 @@
 		},
 		"unboundApiKey": "Unbound API-Schlüssel",
 		"getUnboundApiKey": "Unbound API-Schlüssel erhalten",
+		"unboundRefreshModelsSuccess": "Modellliste aktualisiert! Sie können jetzt aus den neuesten Modellen auswählen.",
+		"unboundInvalidApiKey": "Ungültiger API-Schlüssel. Bitte überprüfen Sie Ihren API-Schlüssel und versuchen Sie es erneut.",
 		"humanRelay": {
 			"description": "Es ist kein API-Schlüssel erforderlich, aber der Benutzer muss beim Kopieren und Einfügen der Informationen in den Web-Chat-KI helfen.",
 			"instructions": "Während der Verwendung wird ein Dialogfeld angezeigt und die aktuelle Nachricht wird automatisch in die Zwischenablage kopiert. Du musst diese in Web-Versionen von KI (wie ChatGPT oder Claude) einfügen, dann die Antwort der KI zurück in das Dialogfeld kopieren und auf die Bestätigungsschaltfläche klicken."

+ 2 - 0
webview-ui/src/i18n/locales/en/settings.json

@@ -185,6 +185,8 @@
 		},
 		"unboundApiKey": "Unbound API Key",
 		"getUnboundApiKey": "Get Unbound API Key",
+		"unboundRefreshModelsSuccess": "Models list updated! You can now select from the latest models.",
+		"unboundInvalidApiKey": "Invalid API key. Please check your API key and try again.",
 		"humanRelay": {
 			"description": "No API key is required, but the user needs to help copy and paste the information to the web chat AI.",
 			"instructions": "During use, a dialog box will pop up and the current message will be copied to the clipboard automatically. You need to paste these to web versions of AI (such as ChatGPT or Claude), then copy the AI's reply back to the dialog box and click the confirm button."

+ 2 - 0
webview-ui/src/i18n/locales/es/settings.json

@@ -185,6 +185,8 @@
 		},
 		"unboundApiKey": "Clave API de Unbound",
 		"getUnboundApiKey": "Obtener clave API de Unbound",
+		"unboundRefreshModelsSuccess": "¡Lista de modelos actualizada! Ahora puede seleccionar entre los últimos modelos.",
+		"unboundInvalidApiKey": "Clave API inválida. Por favor, verifique su clave API e inténtelo de nuevo.",
 		"humanRelay": {
 			"description": "No se requiere clave API, pero el usuario necesita ayudar a copiar y pegar la información en el chat web de IA.",
 			"instructions": "Durante el uso, aparecerá un cuadro de diálogo y el mensaje actual se copiará automáticamente al portapapeles. Debe pegarlo en las versiones web de IA (como ChatGPT o Claude), luego copiar la respuesta de la IA de vuelta al cuadro de diálogo y hacer clic en el botón de confirmar."

+ 2 - 0
webview-ui/src/i18n/locales/fr/settings.json

@@ -185,6 +185,8 @@
 		},
 		"unboundApiKey": "Clé API Unbound",
 		"getUnboundApiKey": "Obtenir la clé API Unbound",
+		"unboundRefreshModelsSuccess": "Liste des modèles mise à jour ! Vous pouvez maintenant sélectionner parmi les derniers modèles.",
+		"unboundInvalidApiKey": "Clé API invalide. Veuillez vérifier votre clé API et réessayer.",
 		"humanRelay": {
 			"description": "Aucune clé API n'est requise, mais l'utilisateur doit aider à copier et coller les informations dans le chat web de l'IA.",
 			"instructions": "Pendant l'utilisation, une boîte de dialogue apparaîtra et le message actuel sera automatiquement copié dans le presse-papiers. Vous devez le coller dans les versions web de l'IA (comme ChatGPT ou Claude), puis copier la réponse de l'IA dans la boîte de dialogue et cliquer sur le bouton de confirmation."

+ 2 - 0
webview-ui/src/i18n/locales/hi/settings.json

@@ -185,6 +185,8 @@
 		},
 		"unboundApiKey": "Unbound API कुंजी",
 		"getUnboundApiKey": "Unbound API कुंजी प्राप्त करें",
+		"unboundRefreshModelsSuccess": "मॉडल सूची अपडेट हो गई है! अब आप नवीनतम मॉडलों में से चुन सकते हैं।",
+		"unboundInvalidApiKey": "अमान्य API कुंजी। कृपया अपनी API कुंजी की जांच करें और पुनः प्रयास करें।",
 		"humanRelay": {
 			"description": "कोई API कुंजी आवश्यक नहीं है, लेकिन उपयोगकर्ता को वेब चैट AI में जानकारी कॉपी और पेस्ट करने में मदद करनी होगी।",
 			"instructions": "उपयोग के दौरान, एक डायलॉग बॉक्स पॉप अप होगा और वर्तमान संदेश स्वचालित रूप से क्लिपबोर्ड पर कॉपी हो जाएगा। आपको इन्हें AI के वेब संस्करणों (जैसे ChatGPT या Claude) में पेस्ट करना होगा, फिर AI की प्रतिक्रिया को डायलॉग बॉक्स में वापस कॉपी करें और पुष्टि बटन पर क्लिक करें।"

+ 2 - 0
webview-ui/src/i18n/locales/it/settings.json

@@ -185,6 +185,8 @@
 		},
 		"unboundApiKey": "Chiave API Unbound",
 		"getUnboundApiKey": "Ottieni chiave API Unbound",
+		"unboundRefreshModelsSuccess": "Lista dei modelli aggiornata! Ora puoi selezionare tra gli ultimi modelli.",
+		"unboundInvalidApiKey": "Chiave API non valida. Controlla la tua chiave API e riprova.",
 		"humanRelay": {
 			"description": "Non è richiesta alcuna chiave API, ma l'utente dovrà aiutare a copiare e incollare le informazioni nella chat web AI.",
 			"instructions": "Durante l'uso, apparirà una finestra di dialogo e il messaggio corrente verrà automaticamente copiato negli appunti. Dovrai incollarlo nelle versioni web dell'AI (come ChatGPT o Claude), quindi copiare la risposta dell'AI nella finestra di dialogo e fare clic sul pulsante di conferma."

+ 2 - 0
webview-ui/src/i18n/locales/ja/settings.json

@@ -185,6 +185,8 @@
 		},
 		"unboundApiKey": "Unbound APIキー",
 		"getUnboundApiKey": "Unbound APIキーを取得",
+		"unboundRefreshModelsSuccess": "モデルリストが更新されました!最新のモデルから選択できます。",
+		"unboundInvalidApiKey": "無効なAPIキーです。APIキーを確認して、もう一度お試しください。",
 		"humanRelay": {
 			"description": "APIキーは不要ですが、ユーザーはウェブチャットAIに情報をコピー&ペーストする必要があります。",
 			"instructions": "使用中にダイアログボックスが表示され、現在のメッセージが自動的にクリップボードにコピーされます。これらをウェブ版のAI(ChatGPTやClaudeなど)に貼り付け、AIの返答をダイアログボックスにコピーして確認ボタンをクリックする必要があります。"

+ 2 - 0
webview-ui/src/i18n/locales/ko/settings.json

@@ -185,6 +185,8 @@
 		},
 		"unboundApiKey": "Unbound API 키",
 		"getUnboundApiKey": "Unbound API 키 받기",
+		"unboundRefreshModelsSuccess": "모델 목록이 업데이트되었습니다! 이제 최신 모델에서 선택할 수 있습니다.",
+		"unboundInvalidApiKey": "잘못된 API 키입니다. API 키를 확인하고 다시 시도해 주세요.",
 		"humanRelay": {
 			"description": "API 키가 필요하지 않지만, 사용자가 웹 채팅 AI에 정보를 복사하여 붙여넣어야 합니다.",
 			"instructions": "사용 중에 대화 상자가 나타나고 현재 메시지가 자동으로 클립보드에 복사됩니다. 이를 웹 버전 AI(예: ChatGPT 또는 Claude)에 붙여넣은 다음, AI의 응답을 대화 상자에 복사하고 확인 버튼을 클릭해야 합니다."

+ 2 - 0
webview-ui/src/i18n/locales/nl/settings.json

@@ -185,6 +185,8 @@
 		},
 		"unboundApiKey": "Unbound API-sleutel",
 		"getUnboundApiKey": "Unbound API-sleutel ophalen",
+		"unboundRefreshModelsSuccess": "Modellenlijst bijgewerkt! U kunt nu kiezen uit de nieuwste modellen.",
+		"unboundInvalidApiKey": "Ongeldige API-sleutel. Controleer uw API-sleutel en probeer het opnieuw.",
 		"humanRelay": {
 			"description": "Geen API-sleutel vereist, maar de gebruiker moet helpen met kopiëren en plakken naar de webchat-AI.",
 			"instructions": "Tijdens gebruik verschijnt een dialoogvenster en wordt het huidige bericht automatisch naar het klembord gekopieerd. Je moet deze plakken in webversies van AI (zoals ChatGPT of Claude), vervolgens het antwoord van de AI terugkopiëren naar het dialoogvenster en op bevestigen klikken."

+ 2 - 0
webview-ui/src/i18n/locales/pl/settings.json

@@ -185,6 +185,8 @@
 		},
 		"unboundApiKey": "Klucz API Unbound",
 		"getUnboundApiKey": "Uzyskaj klucz API Unbound",
+		"unboundRefreshModelsSuccess": "Lista modeli zaktualizowana! Możesz teraz wybierać spośród najnowszych modeli.",
+		"unboundInvalidApiKey": "Nieprawidłowy klucz API. Sprawdź swój klucz API i spróbuj ponownie.",
 		"humanRelay": {
 			"description": "Nie jest wymagany klucz API, ale użytkownik będzie musiał pomóc w kopiowaniu i wklejaniu informacji do czatu internetowego AI.",
 			"instructions": "Podczas użytkowania pojawi się okno dialogowe, a bieżąca wiadomość zostanie automatycznie skopiowana do schowka. Będziesz musiał wkleić ją do internetowych wersji AI (takich jak ChatGPT lub Claude), a następnie skopiować odpowiedź AI z powrotem do okna dialogowego i kliknąć przycisk potwierdzenia."

+ 2 - 0
webview-ui/src/i18n/locales/pt-BR/settings.json

@@ -185,6 +185,8 @@
 		},
 		"unboundApiKey": "Chave de API Unbound",
 		"getUnboundApiKey": "Obter chave de API Unbound",
+		"unboundRefreshModelsSuccess": "Lista de modelos atualizada! Agora você pode selecionar entre os modelos mais recentes.",
+		"unboundInvalidApiKey": "Chave API inválida. Por favor, verifique sua chave API e tente novamente.",
 		"humanRelay": {
 			"description": "Não é necessária chave de API, mas o usuário precisa ajudar a copiar e colar as informações para a IA do chat web.",
 			"instructions": "Durante o uso, uma caixa de diálogo será exibida e a mensagem atual será copiada para a área de transferência automaticamente. Você precisa colar isso nas versões web de IA (como ChatGPT ou Claude), depois copiar a resposta da IA de volta para a caixa de diálogo e clicar no botão confirmar."

+ 2 - 0
webview-ui/src/i18n/locales/ru/settings.json

@@ -185,6 +185,8 @@
 		},
 		"unboundApiKey": "Unbound API-ключ",
 		"getUnboundApiKey": "Получить Unbound API-ключ",
+		"unboundRefreshModelsSuccess": "Список моделей обновлен! Теперь вы можете выбрать из последних моделей.",
+		"unboundInvalidApiKey": "Недействительный API-ключ. Пожалуйста, проверьте ваш API-ключ и попробуйте снова.",
 		"humanRelay": {
 			"description": "API-ключ не требуется, но пользователю нужно вручную копировать и вставлять информацию в веб-чат ИИ.",
 			"instructions": "Во время использования появится диалоговое окно, и текущее сообщение будет скопировано в буфер обмена автоматически. Вам нужно вставить его в веб-версию ИИ (например, ChatGPT или Claude), затем скопировать ответ ИИ обратно в диалоговое окно и нажать кнопку подтверждения."

+ 2 - 0
webview-ui/src/i18n/locales/tr/settings.json

@@ -185,6 +185,8 @@
 		},
 		"unboundApiKey": "Unbound API Anahtarı",
 		"getUnboundApiKey": "Unbound API Anahtarı Al",
+		"unboundRefreshModelsSuccess": "Model listesi güncellendi! Artık en son modeller arasından seçim yapabilirsiniz.",
+		"unboundInvalidApiKey": "Geçersiz API anahtarı. Lütfen API anahtarınızı kontrol edin ve tekrar deneyin.",
 		"humanRelay": {
 			"description": "API anahtarı gerekmez, ancak kullanıcının bilgileri web sohbet yapay zekasına kopyalayıp yapıştırması gerekir.",
 			"instructions": "Kullanım sırasında bir iletişim kutusu açılacak ve mevcut mesaj otomatik olarak panoya kopyalanacaktır. Bunları web yapay zekalarına (ChatGPT veya Claude gibi) yapıştırmanız, ardından yapay zekanın yanıtını iletişim kutusuna kopyalayıp onay düğmesine tıklamanız gerekir."

+ 2 - 0
webview-ui/src/i18n/locales/vi/settings.json

@@ -185,6 +185,8 @@
 		},
 		"unboundApiKey": "Khóa API Unbound",
 		"getUnboundApiKey": "Lấy khóa API Unbound",
+		"unboundRefreshModelsSuccess": "Đã cập nhật danh sách mô hình! Bây giờ bạn có thể chọn từ các mô hình mới nhất.",
+		"unboundInvalidApiKey": "Khóa API không hợp lệ. Vui lòng kiểm tra khóa API của bạn và thử lại.",
 		"humanRelay": {
 			"description": "Không cần khóa API, nhưng người dùng cần giúp sao chép và dán thông tin vào AI trò chuyện web.",
 			"instructions": "Trong quá trình sử dụng, một hộp thoại sẽ xuất hiện và tin nhắn hiện tại sẽ được tự động sao chép vào clipboard. Bạn cần dán chúng vào các phiên bản web của AI (như ChatGPT hoặc Claude), sau đó sao chép phản hồi của AI trở lại hộp thoại và nhấp vào nút xác nhận."

+ 2 - 0
webview-ui/src/i18n/locales/zh-CN/settings.json

@@ -185,6 +185,8 @@
 		},
 		"unboundApiKey": "Unbound API 密钥",
 		"getUnboundApiKey": "获取 Unbound API 密钥",
+		"unboundRefreshModelsSuccess": "模型列表已更新!您现在可以从最新模型中选择。",
+		"unboundInvalidApiKey": "无效的API密钥。请检查您的API密钥并重试。",
 		"humanRelay": {
 			"description": "不需要 API 密钥,但用户需要帮助将信息复制并粘贴到网页聊天 AI。",
 			"instructions": "使用期间,将弹出对话框并自动将当前消息复制到剪贴板。您需要将这些内容粘贴到 AI 的网页版本(如 ChatGPT 或 Claude),然后将 AI 的回复复制回对话框并点击确认按钮。"

+ 2 - 0
webview-ui/src/i18n/locales/zh-TW/settings.json

@@ -185,6 +185,8 @@
 		},
 		"unboundApiKey": "Unbound API 金鑰",
 		"getUnboundApiKey": "取得 Unbound API 金鑰",
+		"unboundRefreshModelsSuccess": "模型列表已更新!您現在可以從最新模型中選擇。",
+		"unboundInvalidApiKey": "無效的API金鑰。請檢查您的API金鑰並重試。",
 		"humanRelay": {
 			"description": "不需要 API 金鑰,但使用者需要協助將資訊複製並貼上到網頁聊天 AI。",
 			"instructions": "使用期間會彈出對話框,並自動將目前訊息複製到剪貼簿。您需要將這些內容貼上到網頁版 AI(如 ChatGPT 或 Claude),然後將 AI 的回覆複製回對話框並點選確認按鈕。"