wip: initial setup screen
This commit is contained in:
parent
66a4ac420b
commit
9299e772ba
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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',
|
||||
},
|
||||
},
|
||||
|
||||
63
web-app/src/containers/CustomeTooltipJoyRide.tsx
Normal file
63
web-app/src/containers/CustomeTooltipJoyRide.tsx
Normal 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}>
|
||||
×
|
||||
</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>
|
||||
)
|
||||
}
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
64
web-app/src/containers/SetupScreen.tsx
Normal file
64
web-app/src/containers/SetupScreen.tsx
Normal 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, you’ll 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
|
||||
@ -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}
|
||||
|
||||
@ -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)}
|
||||
/>
|
||||
)
|
||||
|
||||
@ -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 \n\n- Linked Image:\n\n [](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',
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 provider’s 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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user