Add Hugging Face to welcome screen

This commit is contained in:
Matt Rubens 2025-09-11 00:10:58 -04:00
parent 912d57c692
commit 1389e6232b
20 changed files with 127 additions and 23 deletions

BIN
src/assets/images/hf.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

View file

@ -2,6 +2,7 @@ import { useCallback, useState } from "react"
import knuthShuffle from "knuth-shuffle-seeded"
import { Trans } from "react-i18next"
import { VSCodeButton, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
import pkceChallenge from "pkce-challenge"
import type { ProviderSettings } from "@roo-code/types"
@ -9,7 +10,7 @@ import { useExtensionState } from "@src/context/ExtensionStateContext"
import { validateApiConfiguration } from "@src/utils/validate"
import { vscode } from "@src/utils/vscode"
import { useAppTranslation } from "@src/i18n/TranslationContext"
import { getRequestyAuthUrl, getOpenRouterAuthUrl } from "@src/oauth/urls"
import { getRequestyAuthUrl, getOpenRouterAuthUrl, getHuggingFaceAuthUrl } from "@src/oauth/urls"
import ApiOptions from "../settings/ApiOptions"
import { Tab, TabContent } from "../common/Tab"
@ -47,23 +48,57 @@ const WelcomeView = () => {
return w.IMAGES_BASE_URI || ""
})
// Handle Hugging Face OAuth with PKCE
const handleHuggingFaceOAuth = useCallback(async () => {
try {
// Generate PKCE challenge using the library
const pkce = await pkceChallenge()
const state = crypto.randomUUID() // Use built-in UUID for state
// Store verifier/state in extension (secrets)
vscode.postMessage({
type: "storeHuggingFacePkce",
values: { verifier: pkce.code_verifier, state },
})
const authUrl = getHuggingFaceAuthUrl(uriScheme, pkce.code_challenge, state)
// Open externally via extension
vscode.postMessage({ type: "openExternal", url: authUrl })
} catch (e) {
console.error("Failed to start Hugging Face OAuth:", e)
}
}, [uriScheme])
// Handle provider click
const handleProviderClick = useCallback(
(provider: any) => {
if (provider.slug === "hf") {
// Hugging Face needs special handling for PKCE
handleHuggingFaceOAuth()
} else {
// Other providers can use direct links
vscode.postMessage({ type: "openExternal", url: provider.authUrl })
}
},
[handleHuggingFaceOAuth],
)
return (
<Tab>
<TabContent className="flex flex-col gap-4 p-6">
<TabContent className="flex flex-col gap-2 p-6">
<RooHero />
<h2 className="mt-0 mb-4 text-xl text-center">{t("welcome:greeting")}</h2>
<h2 className="mt-0 mb-0 text-xl text-center">{t("welcome:greeting")}</h2>
<div className="text-base text-vscode-foreground py-2 px-2 mb-4">
<div className="text-base text-vscode-foreground py-0 px-0 mb-2">
<p className="mb-3 leading-relaxed">
<Trans i18nKey="welcome:introduction" />
</p>
<p className="mb-0 leading-relaxed">
<p className="mb-3 leading-relaxed">
<Trans i18nKey="welcome:chooseProvider" />
&nbsp;
<Trans i18nKey="welcome:startRouter" />
</p>
</div>
<div className="mb-4">
<p className="text-sm font-medium mt-4 mb-3">{t("welcome:startRouter")}</p>
<div>
{/* Define the providers */}
@ -83,6 +118,12 @@ const WelcomeView = () => {
description: t("welcome:routers.openrouter.description"),
authUrl: getOpenRouterAuthUrl(uriScheme),
},
{
slug: "hf",
name: "Hugging Face",
description: t("welcome:routers.huggingface.description"),
authUrl: getHuggingFaceAuthUrl(uriScheme),
},
]
// Shuffle providers based on machine ID (will be consistent for the same machine)
@ -91,12 +132,26 @@ const WelcomeView = () => {
// Render the provider cards
return orderedProviders.map((provider, index) => (
<a
<div
key={index}
href={provider.authUrl}
className="flex-1 border border-vscode-panel-border hover:bg-secondary rounded-md py-3 px-4 mb-2 flex flex-row gap-3 cursor-pointer transition-all no-underline text-inherit"
target="_blank"
rel="noopener noreferrer">
onClick={(e) => {
e.preventDefault()
handleProviderClick(provider)
}}
className="relative flex-1 border border-vscode-panel-border hover:bg-secondary rounded-md py-3 px-4 mb-2 flex flex-row gap-3 cursor-pointer transition-all no-underline text-inherit"
role="button"
tabIndex={0}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault()
handleProviderClick(provider)
}
}}>
{provider.incentive && (
<div className="absolute top-0 right-0 bg-vscode-badge-background text-vscode-badge-foreground px-1.5 py-0.5 rounded-bl rounded-tr-md text-[10px] font-medium">
{provider.incentive}
</div>
)}
<div className="w-8 h-8 flex-shrink-0">
<img
src={`${imagesBaseUri}/${provider.slug}.png`}
@ -108,21 +163,16 @@ const WelcomeView = () => {
<div className="text-sm font-medium text-vscode-foreground">
{provider.name}
</div>
<div>
<div className="text-xs text-vscode-descriptionForeground">
{provider.description}
</div>
{provider.incentive && (
<div className="text-xs mt-1">{provider.incentive}</div>
)}
<div className="text-xs text-vscode-descriptionForeground">
{provider.description}
</div>
</div>
</a>
</div>
))
})()}
</div>
<p className="text-sm font-medium mt-6 mb-3">{t("welcome:startCustom")}</p>
<p className="text-base mt-4 mb-3">{t("welcome:startCustom")}</p>
<ApiOptions
fromWelcomeView
apiConfiguration={apiConfiguration || {}}

View file

@ -10,6 +10,9 @@
},
"openrouter": {
"description": "Una interfície unificada per a LLMs"
},
"huggingface": {
"description": "Un router per als millors models oberts"
}
},
"chooseProvider": "Per fer la seva màgia, Roo necessita una clau API.",

View file

@ -10,6 +10,9 @@
},
"openrouter": {
"description": "Eine einheitliche Schnittstelle für LLMs"
},
"huggingface": {
"description": "Ein Router für die besten Open-Source-Modelle"
}
},
"chooseProvider": "Um seine Magie zu entfalten, benötigt Roo einen API-Schlüssel.",

View file

@ -10,6 +10,9 @@
},
"openrouter": {
"description": "A unified interface for LLMs"
},
"huggingface": {
"description": "A router for the best open models"
}
},
"chooseProvider": "To do its magic, Roo needs an API key.",

View file

@ -10,6 +10,9 @@
},
"openrouter": {
"description": "Una interfaz unificada para LLMs"
},
"huggingface": {
"description": "Un router para los mejores modelos abiertos"
}
},
"chooseProvider": "Para hacer su magia, Roo necesita una clave API.",

View file

@ -10,6 +10,9 @@
},
"openrouter": {
"description": "Une interface unifiée pour les LLMs"
},
"huggingface": {
"description": "Un routeur pour les meilleurs modèles open source"
}
},
"chooseProvider": "Pour faire sa magie, Roo a besoin d'une clé API.",

View file

@ -10,6 +10,9 @@
},
"openrouter": {
"description": "LLMs के लिए एक एकीकृत इंटरफेस"
},
"huggingface": {
"description": "सर्वश्रेष्ठ ओपन मॉडल्स के लिए एक राउटर"
}
},
"chooseProvider": "अपना जादू दिखाने के लिए, Roo को एक API कुंजी की आवश्यकता है।",

View file

@ -10,6 +10,9 @@
},
"openrouter": {
"description": "Interface terpadu untuk LLM"
},
"huggingface": {
"description": "Router untuk model terbuka terbaik"
}
},
"chooseProvider": "Untuk melakukan keajaibannya, Roo membutuhkan API key.",

View file

@ -10,6 +10,9 @@
},
"openrouter": {
"description": "Un'interfaccia unificata per LLMs"
},
"huggingface": {
"description": "Un router per i migliori modelli open source"
}
},
"chooseProvider": "Per fare la sua magia, Roo ha bisogno di una chiave API.",

View file

@ -10,6 +10,9 @@
},
"openrouter": {
"description": "LLMsのための統一インターフェース"
},
"huggingface": {
"description": "最高のオープンモデルのためのルーター"
}
},
"chooseProvider": "Rooが機能するには、APIキーが必要です。",

View file

@ -10,6 +10,9 @@
},
"openrouter": {
"description": "LLM을 위한 통합 인터페이스"
},
"huggingface": {
"description": "최고의 오픈 모델을 위한 라우터"
}
},
"chooseProvider": "Roo가 작동하려면 API 키가 필요합니다.",

View file

@ -10,6 +10,9 @@
},
"openrouter": {
"description": "Een uniforme interface voor LLM's"
},
"huggingface": {
"description": "Een router voor de beste open modellen"
}
},
"chooseProvider": "Om zijn magie te doen, heeft Roo een API-sleutel nodig.",

View file

@ -10,6 +10,9 @@
},
"openrouter": {
"description": "Ujednolicony interfejs dla LLMs"
},
"huggingface": {
"description": "Router dla najlepszych otwartych modeli"
}
},
"chooseProvider": "Aby działać, Roo potrzebuje klucza API.",

View file

@ -10,6 +10,9 @@
},
"openrouter": {
"description": "Uma interface unificada para LLMs"
},
"huggingface": {
"description": "Um roteador para os melhores modelos abertos"
}
},
"chooseProvider": "Para fazer sua mágica, o Roo precisa de uma chave API.",

View file

@ -10,6 +10,9 @@
},
"openrouter": {
"description": "Унифицированный интерфейс для LLM"
},
"huggingface": {
"description": "Маршрутизатор для лучших открытых моделей"
}
},
"chooseProvider": "Для своей магии Roo нуждается в API-ключе.",

View file

@ -10,6 +10,9 @@
},
"openrouter": {
"description": "LLM'ler için birleşik bir arayüz"
},
"huggingface": {
"description": "En iyi açık modeller için bir yönlendirici"
}
},
"chooseProvider": "Sihirini yapabilmesi için Roo'nun bir API anahtarına ihtiyacı var.",

View file

@ -10,6 +10,9 @@
},
"openrouter": {
"description": "Giao diện thống nhất cho các LLM"
},
"huggingface": {
"description": "Bộ định tuyến cho các mô hình mở tốt nhất"
}
},
"chooseProvider": "Để thực hiện phép màu của mình, Roo cần một khóa API.",

View file

@ -10,6 +10,9 @@
},
"openrouter": {
"description": "统一了大语言模型的接口"
},
"huggingface": {
"description": "开源模型路由器"
}
},
"chooseProvider": "Roo 需要一个 API 密钥才能发挥魔力。",

View file

@ -10,6 +10,9 @@
},
"openrouter": {
"description": "LLM 的統一介面"
},
"huggingface": {
"description": "最佳開放模型的路由器"
}
},
"chooseProvider": "Roo 需要 API 金鑰才能發揮魔力。",