wip: initial setup screen

This commit is contained in:
Faisal Amir 2025-05-15 23:32:53 +07:00
parent 66a4ac420b
commit 9299e772ba
11 changed files with 441 additions and 302 deletions

View File

@ -68,5 +68,8 @@
"resolutions": {
"yallist": "4.0.0"
},
"packageManager": "yarn@4.5.3"
"packageManager": "yarn@4.5.3",
"dependencies": {
"@tanstack/router": "0.0.1-beta.53"
}
}

View File

@ -47,6 +47,7 @@
"react-colorful": "^5.6.1",
"react-dom": "^19.0.0",
"react-i18next": "^15.5.1",
"react-joyride": "^2.9.3",
"react-markdown": "^10.1.0",
"react-syntax-highlighter": "^15.6.1",
"react-syntax-highlighter-virtualized-renderer": "^1.1.0",

View File

@ -12,12 +12,12 @@ const buttonVariants = cva(
default: 'bg-primary text-primary-fg shadow-xs hover:bg-primary/90',
destructive:
'bg-destructive shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 text-destructive-fg',
link: 'underline-offset-4 hover:underline',
link: 'underline-offset-4 hover:no-underline',
},
size: {
default: 'h-7 px-3 py-2 has-[>svg]:px-3 rounded-sm',
sm: 'h-6 rounded gap-1.5 px-2 has-[>svg]:px-2.5',
lg: 'h-9 rounded-md px-6 has-[>svg]:px-4',
lg: 'h-9 rounded-md px-4 has-[>svg]:px-4',
icon: 'size-8',
},
},

View File

@ -0,0 +1,63 @@
import { TooltipRenderProps } from 'react-joyride'
import { Button } from '@/components/ui/button'
import { cn } from '@/lib/utils'
export function CustomTooltipJoyRide(props: TooltipRenderProps) {
const {
backProps,
closeProps,
continuous,
index,
primaryProps,
skipProps,
step,
tooltipProps,
} = props
return (
<div
className="bg-main-view p-4 rounded-xl max-w-[400px] text-main-view-fg relative select-none"
{...tooltipProps}
>
{!step.hideCloseButton && (
<div className="absolute size-4 top-1 right-2 cursor-pointer">
<button className="text-right" {...closeProps}>
&times;
</button>
</div>
)}
{step.title && <h4 className="text-base mb-2">{step.title}</h4>}
<div className="text-sm text-main-view-fg/70 leading-relaxed">
{step.content}
</div>
<div
className={cn(
'flex items-center justify-end mt-2',
step.showSkipButton && 'justify-between'
)}
>
{step.showSkipButton && (
<Button variant="link" className="px-0" {...skipProps}>
{skipProps.title}
</Button>
)}
<div className={cn('flex items-center justify-between gap-4')}>
{index > 0 && (
<Button
variant="link"
className="px-0 text-main-view-fg/60"
{...backProps}
>
{backProps.title}
</Button>
)}
{continuous && (
<Button size="sm" {...primaryProps}>
{primaryProps.title}
</Button>
)}
</div>
</div>
</div>
)
}

View File

@ -4,7 +4,11 @@ import { cn, getProviderLogo, getProviderTitle } from '@/lib/utils'
import { useNavigate, useMatches, Link } from '@tanstack/react-router'
import { IconArrowLeft } from '@tabler/icons-react'
const ProvidersMenu = () => {
const ProvidersMenu = ({
stepSetupRemoteProvider,
}: {
stepSetupRemoteProvider: boolean
}) => {
const { providers } = useModelProvider()
const navigate = useNavigate()
const matches = useMatches()
@ -17,40 +21,51 @@ const ProvidersMenu = () => {
<span className="text-main-view-fg/80">Back</span>
</div>
</Link>
{providers.map((provider, index) => {
const isActive = matches.some(
(match) =>
match.routeId === '/settings/providers/$providerName' &&
'providerName' in match.params &&
match.params.providerName === provider.provider
)
<div className="first-step-setup-remote-provider">
{providers.map((provider, index) => {
const isActive = matches.some(
(match) =>
match.routeId === '/settings/providers/$providerName' &&
'providerName' in match.params &&
match.params.providerName === provider.provider
)
return (
<div key={index} className="flex flex-col px-2 my-1.5 ">
<div
className={cn(
'flex px-2 items-center gap-1.5 cursor-pointer hover:bg-main-view-fg/5 py-1 w-full rounded [&.active]:bg-main-view-fg/5 text-main-view-fg/80',
isActive && 'bg-main-view-fg/5'
)}
onClick={() =>
navigate({
to: route.settings.providers,
params: { providerName: provider.provider },
})
}
>
<img
src={getProviderLogo(provider.provider)}
alt={`${provider.provider} - Logo`}
className="size-4"
/>
<span className="capitalize">
{getProviderTitle(provider.provider)}
</span>
return (
<div key={index} className="flex flex-col px-2 my-1.5 ">
<div
className={cn(
'flex px-2 items-center gap-1.5 cursor-pointer hover:bg-main-view-fg/5 py-1 w-full rounded [&.active]:bg-main-view-fg/5 text-main-view-fg/80',
isActive && 'bg-main-view-fg/5',
// hidden for llama.cpp provider for setup remote provider
provider.provider === 'llama.cpp' &&
stepSetupRemoteProvider &&
'hidden'
)}
onClick={() =>
navigate({
to: route.settings.providers,
params: {
providerName: provider.provider,
},
...(stepSetupRemoteProvider
? { search: { step: 'setup_remote_provider' } }
: {}),
})
}
>
<img
src={getProviderLogo(provider.provider)}
alt={`${provider.provider} - Logo`}
className="size-4"
/>
<span className="capitalize">
{getProviderTitle(provider.provider)}
</span>
</div>
</div>
</div>
)
})}
)
})}
</div>
</div>
)
}

View File

@ -0,0 +1,64 @@
import DropdownModelProvider from './DropdownModelProvider'
import HeaderPage from './HeaderPage'
import { Card } from './Card'
import { useModelProvider } from '@/hooks/useModelProvider'
import { Link } from '@tanstack/react-router'
import { route } from '@/constants/routes'
function SetupScreen() {
const { providers } = useModelProvider()
const firstItemRemoteProvider =
providers.length > 0 ? providers[1].provider : 'openai'
return (
<div className="flex h-full flex-col flex-justify-center">
<HeaderPage>
<DropdownModelProvider />
</HeaderPage>
<div className="h-full px-8 overflow-y-auto flex flex-col gap-2 justify-center ">
<div className="w-4/6 mx-auto">
<div className="mb-8 text-left">
<h1 className="font-editorialnew text-main-view-fg text-4xl">
Welcome to Jan
</h1>
<p className="text-main-view-fg/70 text-lg mt-2">
To get started, youll need to either download a local AI model or
connect to a cloud model using an API key
</p>
</div>
<div className="flex gap-4 flex-col">
<Card
header={
<div>
<h1 className="text-main-view-fg font-medium text-base">
Setup Local Model
</h1>
</div>
}
></Card>
<Card
header={
<Link
to={route.settings.providers}
params={{
providerName: firstItemRemoteProvider,
}}
search={{
step: 'setup_remote_provider',
}}
>
<h1 className="text-main-view-fg font-medium text-base">
Setup Remote Provider
</h1>
</Link>
}
></Card>
</div>
</div>
</div>
</div>
)
}
export default SetupScreen

View File

@ -9,6 +9,7 @@ type InputControl = {
value: string
onChange: (value: string) => void
inputActions?: string[]
className?: string
}
export function InputControl({
@ -16,6 +17,7 @@ export function InputControl({
placeholder = '',
value = '',
onChange,
className,
inputActions = [],
}: InputControl) {
const [showPassword, setShowPassword] = useState(false)
@ -30,7 +32,13 @@ export function InputControl({
const inputType = type === 'password' && showPassword ? 'text' : type
return (
<div className={cn('relative', type === 'number' ? 'w-16' : 'w-full')}>
<div
className={cn(
'relative',
type === 'number' ? 'w-16' : 'w-full',
className
)}
>
<Input
type={inputType}
placeholder={placeholder}

View File

@ -8,6 +8,7 @@ import { SliderControl } from '@/containers/dynamicControllerSetting/SliderContr
type DynamicControllerProps = {
key?: string
title?: string
className?: string
description?: string
controllerType: 'input' | 'checkbox' | 'dropdown' | 'textarea' | 'slider'
controllerProps: {
@ -25,6 +26,7 @@ type DynamicControllerProps = {
}
export function DynamicControllerSetting({
className,
controllerType,
controllerProps,
onChange,
@ -36,6 +38,7 @@ export function DynamicControllerSetting({
placeholder={controllerProps.placeholder}
value={(controllerProps.value as string) || ''}
inputActions={controllerProps.input_actions}
className={className}
onChange={(newValue) => onChange(newValue)}
/>
)

View File

@ -433,116 +433,3 @@ export const mockModelProvider = [
// ],
// },
]
export const mockTheads = [
{
id: '1',
title: 'Ultimate Markdown Demonstration',
isFavorite: false,
content: [
{
role: 'user',
type: 'text',
text: {
value: 'Dow u know Ultimate Markdown Demonstration',
annotations: [],
},
},
{
type: 'text',
role: 'system',
text: {
value:
'# :books: Ultimate Markdown Demonstration\n\nWelcome to the **Ultimate Markdown Demo**! This document covers a wide range of Markdown features.\n\n---\n\n## 1. Headings\n\n# H1\n## H2\n### H3\n#### H4\n##### H5\n###### H6\n\n---\n\n## 2. Text Formatting\n\n- **Bold**\n- *Italic*\n- ***Bold & Italic***\n- ~~Strikethrough~~\n\n> "Markdown is _awesome_!" — *Someone Famous*\n\n---\n\n## 3. Lists\n\n### 3.1. Unordered List\n\n- Item One\n - Subitem A\n - Subitem B\n - Sub-Subitem i\n\n### 3.2. Ordered List\n\n1. First\n2. Second\n 1. Second-First\n 2. Second-Second\n3. Third\n\n---\n\n## 4. Links and Images\n\n- [Visit OpenAI](https://openai.com)\n- Inline Image:\n\n ![Markdown Logo](https://jan.ai/assets/images/general/logo-mark.svg)\n\n- Linked Image:\n\n [![Markdown Badge](https://img.shields.io/badge/Markdown-Ready-blue)](https://commonmark.org)\n\n---\n\n## 5. Code\n\n### 5.1. Inline Code\n\nUse the `print()` function in Python.\n\n### 5.2. Code Block\n\n```python\ndef greet(name):\n return f"Hello, {name}!"\n\nprint(greet("Markdown"))\n```\n\n### 5.3. Syntax Highlighting (JavaScript)\n\n```javascript\nconst add = (a, b) => a + b;\nconsole.log(add(5, 3));\n```\n\n---\n\n## 6. Tables\n\n| Syntax | Description | Example |\n|--------|-------------|--------|\n| Header | Title | Here\'s this |\n| Paragraph | Text | And more text |\n\n---\n\n## 7. Blockquotes\n\n> "A blockquote can be used to highlight information or quotes."\n\nNested Blockquote:\n\nLevel 1\n>Level 2\nLevel 3\n\n---\n\n## 8. Task Lists\n\n- [x] Write Markdown\n- [x] Check the output\n- [ ] Celebrate\n\n---\n\n## 9. Footnotes\n\nHere is a simple footnote[^1].\n\n[^1]: This is the footnote explanation.\n\n---\n\n## 10. Horizontal Rules\n\n---\n\n## 11. Emojis\n\n:tada: :sunglasses: :potable_water: :books:\n\n---\n\n## 12. Math (Using LaTeX)\n\nInline math: \\( E = mc^2 \\)\n\nBlock math:\n\n$$\n\\int_0^\\infty e^{-x^2} dx = \\frac{\\sqrt{\\pi}}{2}\n$$\n\n---\n\n## 13. HTML in Markdown\n\nSometimes you need raw HTML:\n\n<div style="color:blue; font-weight:bold;">This is blue bold text using HTML inside Markdown!</div>\n\n---\n\n# :dart: That\'s a Wrap!\n\nCongratulations, you\'ve seen nearly every feature Markdown supports!',
annotations: [],
},
},
],
model: {
id: 'gpt-4o',
provider: 'openai',
},
},
{
id: '2',
title: 'Modern JavaScript: A Comprehensive Guide',
isFavorite: false,
content: [
{
role: 'user',
type: 'text',
text: {
value: 'Explain modern JavaScript',
annotations: [],
},
},
{
type: 'text',
role: 'system',
text: {
value:
"# Modern JavaScript: A Comprehensive Guide\n\nThis guide covers essential concepts and features of modern JavaScript that every developer should know.\n\n## ES6+ Features\n\n### Arrow Functions\n\nArrow functions provide a concise syntax for writing functions and lexically bind the `this` value.\n\n```javascript\n// Traditional function\nfunction add(a, b) {\n return a + b;\n}\n\n// Arrow function\nconst add = (a, b) => a + b;\n\n// With implicit return\nconst numbers = [1, 2, 3, 4];\nconst doubled = numbers.map(n => n * 2); // [2, 4, 6, 8]\n```\n\n### Destructuring\n\nDestructuring allows you to extract values from arrays or properties from objects into distinct variables.\n\n```javascript\n// Array destructuring\nconst [first, second, ...rest] = [1, 2, 3, 4, 5];\nconsole.log(first); // 1\nconsole.log(second); // 2\nconsole.log(rest); // [3, 4, 5]\n\n// Object destructuring\nconst person = { name: 'John', age: 30, city: 'New York' };\nconst { name, age, city: location } = person;\nconsole.log(name); // 'John'\nconsole.log(age); // 30\nconsole.log(location); // 'New York'\n```\n\n### Spread and Rest Operators\n\nThe spread operator (`...`) allows an iterable to be expanded in places where zero or more arguments or elements are expected.\n\n```javascript\n// Spread with arrays\nconst arr1 = [1, 2, 3];\nconst arr2 = [...arr1, 4, 5]; // [1, 2, 3, 4, 5]\n\n// Spread with objects\nconst obj1 = { a: 1, b: 2 };\nconst obj2 = { ...obj1, c: 3 }; // { a: 1, b: 2, c: 3 }\n\n// Rest parameter\nfunction sum(...numbers) {\n return numbers.reduce((total, num) => total + num, 0);\n}\nconsole.log(sum(1, 2, 3, 4)); // 10\n```\n\n## Asynchronous JavaScript\n\n### Promises\n\nPromises represent the eventual completion (or failure) of an asynchronous operation and its resulting value.\n\n```javascript\nconst fetchData = () => {\n return new Promise((resolve, reject) => {\n // Simulating an API call\n setTimeout(() => {\n const data = { id: 1, name: 'User' };\n if (data) {\n resolve(data);\n } else {\n reject('Error fetching data');\n }\n }, 1000);\n });\n};\n\nfetchData()\n .then(data => console.log(data))\n .catch(error => console.error(error));\n```\n\n### Async/Await\n\nAsync/await is syntactic sugar built on top of promises, making asynchronous code look and behave more like synchronous code.\n\n```javascript\nconst fetchUser = async (id) => {\n try {\n const response = await fetch(`https://api.example.com/users/${id}`);\n if (!response.ok) throw new Error('Network response was not ok');\n const user = await response.json();\n return user;\n } catch (error) {\n console.error('Error fetching user:', error);\n throw error;\n }\n};\n\n// Using the async function\n(async () => {\n try {\n const user = await fetchUser(1);\n console.log(user);\n } catch (error) {\n console.error(error);\n }\n})();\n```\n\n## Modern JavaScript Patterns\n\n### Module Pattern\n\nES modules provide a way to organize and structure code in separate files.\n\n```javascript\n// math.js\nexport const add = (a, b) => a + b;\nexport const subtract = (a, b) => a - b;\n\n// main.js\nimport { add, subtract } from './math.js';\nconsole.log(add(5, 3)); // 8\n```\n\n### Optional Chaining\n\nOptional chaining (`?.`) allows reading the value of a property located deep within a chain of connected objects without having to check if each reference in the chain is valid.\n\n```javascript\nconst user = {\n name: 'John',\n address: {\n street: '123 Main St',\n city: 'New York'\n }\n};\n\n// Without optional chaining\nconst city = user && user.address && user.address.city;\n\n// With optional chaining\nconst city = user?.address?.city;\n```\n\n## Conclusion\n\nModern JavaScript has evolved significantly with ES6+ features, making code more concise, readable, and maintainable. Understanding these concepts is essential for any JavaScript developer working on modern web applications.",
annotations: [],
},
},
],
model: {
id: 'llama3.2:3b',
provider: 'llama.cpp',
},
},
{
id: '3',
title: 'Reasoning and Tools',
isFavorite: false,
content: [
{
completed_at: 1746419535.019,
role: 'user',
text: {
annotations: [],
value: 'Ask question from user',
},
type: 'text',
created_at: 1746419535.019,
id: '01JTFBEK5BBZ9Y63275WDKRF6D',
metadata: {},
},
{
completed_at: 1746419535.019,
role: 'assistant',
text: {
annotations: [],
value: "I'll read the README.md file using the `read_file` function.",
},
type: 'text',
created_at: 1746419535.019,
id: '01JTFBEK5BBZ9Y63275WDKRF6D',
metadata: {
token_speed: 3.5555555555555554,
tool_calls: [
{
response: {
content: [{ text: '# Jan - Local AI Assistant', type: 'text' }],
},
state: 'ready',
tool: {
function: {
arguments:
'{"path": "/Users/louis/Repositories/jan/README.md"}',
name: 'read_file',
},
id: '01JTFBEN8ZXNM9KB2CM9AY9ZBM',
type: 'function',
},
},
],
},
},
],
model: {
id: 'llama3.2:3b',
provider: 'llama.cpp',
},
},
]

View File

@ -4,6 +4,8 @@ import ChatInput from '@/containers/ChatInput'
import HeaderPage from '@/containers/HeaderPage'
import { useTranslation } from 'react-i18next'
import DropdownModelProvider from '@/containers/DropdownModelProvider'
import { useModelProvider } from '@/hooks/useModelProvider'
import SetupScreen from '@/containers/SetupScreen'
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const Route = createFileRoute(route.home as any)({
@ -12,6 +14,19 @@ export const Route = createFileRoute(route.home as any)({
function Index() {
const { t } = useTranslation()
const { providers } = useModelProvider()
// Conditional to check if there are any valid providers
// required min 1 api_key or 1 model in llama.cpp
const hasValidProviders = providers.some(
(provider) =>
provider.api_key?.length ||
(provider.provider === 'llama.cpp' && provider.models.length)
)
if (!hasValidProviders) {
return <SetupScreen />
}
return (
<div className="flex h-full flex-col flex-justify-center">
@ -28,7 +43,6 @@ function Index() {
{t('chat.description', { ns: 'chat' })}
</p>
</div>
<div className="flex-1 shrink-0">
<ChatInput showSpeedToken={false} />
</div>

View File

@ -1,186 +1,267 @@
import { Card, CardItem } from '@/containers/Card'
import HeaderPage from '@/containers/HeaderPage'
import ProvidersMenu from '@/containers/ProvidersMenu'
import { useModelProvider } from '@/hooks/useModelProvider'
import { getProviderTitle } from '@/lib/utils'
import { cn, getProviderTitle } from '@/lib/utils'
import { Switch } from '@/components/ui/switch'
import { createFileRoute, useParams } from '@tanstack/react-router'
import {
createFileRoute,
useNavigate,
useParams,
useSearch,
} from '@tanstack/react-router'
import { t } from 'i18next'
import Capabilities from '@/containers/Capabilities'
import { DynamicControllerSetting } from '@/containers/dynamicControllerSetting'
import { RenderMarkdown } from '@/containers/RenderMarkdown'
import { DialogEditModel } from '@/containers/dialogs/EditModel'
import { DialogAddModel } from '@/containers/dialogs/AddModel'
import { ModelSetting } from '@/containers/ModelSetting'
import { DialoDeleteModel } from '@/containers/dialogs/DeleteModel'
import Joyride, { CallBackProps, STATUS } from 'react-joyride'
import { CustomTooltipJoyRide } from '@/containers/CustomeTooltipJoyRide'
import { route } from '@/constants/routes'
// as route.threadsDetail
export const Route = createFileRoute('/settings/providers/$providerName')({
component: ProviderDetail,
validateSearch: (search: Record<string, unknown>): { step?: string } => {
// validate and parse the search params into a typed state
return {
step: String(search?.step),
}
},
})
const steps = [
{
target: '.first-step-setup-remote-provider',
title: 'Choose a Provider',
disableBeacon: true,
content:
'Pick the provider you want to use, make sure you have access to an API key for it.',
},
{
target: '.second-step-setup-remote-provider',
title: 'Get Your API Key',
disableBeacon: true,
content:
'Log into the providers dashboard to find or generate your API key.',
},
{
target: '.third-step-setup-remote-provider',
title: 'Insert Your API Key',
disableBeacon: true,
content: 'Paste your API key here to connect and activate the provider.',
},
]
function ProviderDetail() {
const { step } = useSearch({ from: Route.id })
const { providerName } = useParams({ from: Route.id })
const { getProviderByName, updateProvider } = useModelProvider()
const provider = getProviderByName(providerName)
const isSetup = step === 'setup_remote_provider'
const navigate = useNavigate()
const handleJoyrideCallback = (data: CallBackProps) => {
const { status } = data
if (status === STATUS.FINISHED) {
navigate({
to: route.home,
})
}
}
return (
<div className="flex flex-col h-full">
<HeaderPage>
<h1 className="font-medium">{t('common.settings')}</h1>
</HeaderPage>
<div className="flex h-full w-full">
<div className="flex">
<ProvidersMenu />
</div>
<div className="p-4 w-full h-[calc(100%-32px)] overflow-y-auto">
<div className="flex flex-col justify-between gap-4 gap-y-3 w-full">
<div className="flex items-center justify-between">
<h1 className="font-medium text-base">
{getProviderTitle(providerName)}
</h1>
<Switch
checked={provider?.active}
onCheckedChange={(e) => {
if (provider) {
updateProvider(providerName, { ...provider, active: e })
}
}}
/>
</div>
{/* Settings */}
<Card>
{provider?.settings.map((setting, settingIndex) => {
// Use the DynamicController component
const actionComponent = (
<div className="mt-2">
<DynamicControllerSetting
controllerType={setting.controller_type}
controllerProps={setting.controller_props}
onChange={(newValue) => {
if (provider) {
const newSettings = [...provider.settings]
// Handle different value types by forcing the type
// Use type assertion to bypass type checking
// Disable eslint for this line as we need to use type assertion
;(
newSettings[settingIndex].controller_props as {
value: string | boolean | number
}
).value = newValue
// Create update object with updated settings
const updateObj: Partial<ModelProvider> = {
settings: newSettings,
}
// Check if this is an API key or base URL setting and update the corresponding top-level field
const settingKey = setting.key
if (
settingKey === 'api-key' &&
typeof newValue === 'string'
) {
updateObj.api_key = newValue
} else if (
settingKey === 'base-url' &&
typeof newValue === 'string'
) {
updateObj.base_url = newValue
}
updateProvider(providerName, {
...provider,
...updateObj,
})
}
}}
/>
</div>
)
return (
<CardItem
key={settingIndex}
title={setting.title}
column={
setting.controller_type === 'input' &&
setting.controller_props.type !== 'number'
? true
: false
<>
<Joyride
run={isSetup}
floaterProps={{
hideArrow: true,
}}
steps={steps}
tooltipComponent={CustomTooltipJoyRide}
spotlightPadding={0}
continuous={true}
showSkipButton={false}
hideCloseButton={true}
spotlightClicks={true}
disableOverlayClose={true}
callback={handleJoyrideCallback}
locale={{
back: 'Back',
close: 'Close',
last: 'Finish',
next: 'Next',
skip: 'Skip',
}}
/>
<div className="flex flex-col h-full">
<HeaderPage>
<h1 className="font-medium">{t('common.settings')}</h1>
</HeaderPage>
<div className="flex h-full w-full">
<div className="flex">
<ProvidersMenu stepSetupRemoteProvider={isSetup} />
</div>
<div className="p-4 w-full h-[calc(100%-32px)] overflow-y-auto">
<div className="flex flex-col justify-between gap-4 gap-y-3 w-full">
<div className="flex items-center justify-between">
<h1 className="font-medium text-base">
{getProviderTitle(providerName)}
</h1>
<Switch
checked={provider?.active}
onCheckedChange={(e) => {
if (provider) {
updateProvider(providerName, { ...provider, active: e })
}
description={
<RenderMarkdown
className="![>p]:text-main-view-fg/70"
content={setting.description}
components={{
// Make links open in a new tab
a: ({ ...props }) => (
<a
{...props}
target="_blank"
rel="noopener noreferrer"
/>
),
p: ({ ...props }) => (
<p {...props} className="!mb-0" />
),
}}
/>
</div>
{/* Settings */}
<Card>
{provider?.settings.map((setting, settingIndex) => {
// Use the DynamicController component
const actionComponent = (
<div className="mt-2">
<DynamicControllerSetting
controllerType={setting.controller_type}
controllerProps={setting.controller_props}
className={cn(
setting.key === 'api-key' &&
'third-step-setup-remote-provider'
)}
onChange={(newValue) => {
if (provider) {
const newSettings = [...provider.settings]
// Handle different value types by forcing the type
// Use type assertion to bypass type checking
;(
newSettings[settingIndex].controller_props as {
value: string | boolean | number
}
).value = newValue
// Create update object with updated settings
const updateObj: Partial<ModelProvider> = {
settings: newSettings,
}
// Check if this is an API key or base URL setting and update the corresponding top-level field
const settingKey = setting.key
if (
settingKey === 'api-key' &&
typeof newValue === 'string'
) {
updateObj.api_key = newValue
} else if (
settingKey === 'base-url' &&
typeof newValue === 'string'
) {
updateObj.base_url = newValue
}
updateProvider(providerName, {
...provider,
...updateObj,
})
}
}}
/>
}
actions={actionComponent}
/>
)
})}
</Card>
</div>
)
{/* Models */}
<Card
header={
<div className="flex items-center justify-between mb-4">
<h1 className="text-main-view-fg font-medium text-base">
Models
</h1>
<div className="flex items-center gap-2">
{provider && <DialogAddModel provider={provider} />}
return (
<CardItem
key={settingIndex}
title={setting.title}
column={
setting.controller_type === 'input' &&
setting.controller_props.type !== 'number'
? true
: false
}
description={
<RenderMarkdown
className="![>p]:text-main-view-fg/70 select-none"
content={setting.description}
components={{
// Make links open in a new tab
a: ({ ...props }) => {
return (
<a
{...props}
target="_blank"
rel="noopener noreferrer"
className={cn(
setting.key === 'api-key' &&
'second-step-setup-remote-provider'
)}
/>
)
},
p: ({ ...props }) => (
<p {...props} className="!mb-0" />
),
}}
/>
}
actions={actionComponent}
/>
)
})}
</Card>
{/* Models */}
<Card
header={
<div className="flex items-center justify-between mb-4">
<h1 className="text-main-view-fg font-medium text-base">
Models
</h1>
<div className="flex items-center gap-2">
{provider && <DialogAddModel provider={provider} />}
</div>
</div>
</div>
}
>
{provider?.models.map((model, modelIndex) => {
const capabilities = model.capabilities || []
return (
<CardItem
key={modelIndex}
title={
<div className="flex items-center gap-2">
<h1 className="font-medium">{model.id}</h1>
<Capabilities capabilities={capabilities} />
</div>
}
actions={
<div className="flex items-center gap-2">
<DialogEditModel
provider={provider}
modelId={model.id}
/>
{model.settings && (
<ModelSetting provider={provider} model={model} />
)}
<DialoDeleteModel
provider={provider}
modelId={model.id}
/>
</div>
}
/>
)
})}
</Card>
}
>
{provider?.models.map((model, modelIndex) => {
const capabilities = model.capabilities || []
return (
<CardItem
key={modelIndex}
title={
<div className="flex items-center gap-2">
<h1 className="font-medium">{model.id}</h1>
<Capabilities capabilities={capabilities} />
</div>
}
actions={
<div className="flex items-center gap-2">
<DialogEditModel
provider={provider}
modelId={model.id}
/>
{model.settings && (
<ModelSetting provider={provider} model={model} />
)}
<DialoDeleteModel
provider={provider}
modelId={model.id}
/>
</div>
}
/>
)
})}
</Card>
</div>
</div>
</div>
</div>
</div>
</>
)
}