diff --git a/package.json b/package.json index ba94d0a93..6a758a403 100644 --- a/package.json +++ b/package.json @@ -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" + } } diff --git a/web-app/package.json b/web-app/package.json index 87c0b47e7..0ad217f75 100644 --- a/web-app/package.json +++ b/web-app/package.json @@ -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", diff --git a/web-app/src/components/ui/button.tsx b/web-app/src/components/ui/button.tsx index fc78d306b..2027ac7b6 100644 --- a/web-app/src/components/ui/button.tsx +++ b/web-app/src/components/ui/button.tsx @@ -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', }, }, diff --git a/web-app/src/containers/CustomeTooltipJoyRide.tsx b/web-app/src/containers/CustomeTooltipJoyRide.tsx new file mode 100644 index 000000000..6443c6f28 --- /dev/null +++ b/web-app/src/containers/CustomeTooltipJoyRide.tsx @@ -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 ( +
+ {!step.hideCloseButton && ( +
+ +
+ )} + {step.title &&

{step.title}

} +
+ {step.content} +
+
+ {step.showSkipButton && ( + + )} +
+ {index > 0 && ( + + )} + {continuous && ( + + )} +
+
+
+ ) +} diff --git a/web-app/src/containers/ProvidersMenu.tsx b/web-app/src/containers/ProvidersMenu.tsx index 6e380d385..2b36f4f59 100644 --- a/web-app/src/containers/ProvidersMenu.tsx +++ b/web-app/src/containers/ProvidersMenu.tsx @@ -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 = () => { Back - {providers.map((provider, index) => { - const isActive = matches.some( - (match) => - match.routeId === '/settings/providers/$providerName' && - 'providerName' in match.params && - match.params.providerName === provider.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 ( -
-
- navigate({ - to: route.settings.providers, - params: { providerName: provider.provider }, - }) - } - > - {`${provider.provider} - - {getProviderTitle(provider.provider)} - + return ( +
+
-
- ) - })} + ) + })} +
) } diff --git a/web-app/src/containers/SetupScreen.tsx b/web-app/src/containers/SetupScreen.tsx new file mode 100644 index 000000000..6fca44acb --- /dev/null +++ b/web-app/src/containers/SetupScreen.tsx @@ -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 ( +
+ + + +
+
+
+

+ Welcome to Jan +

+

+ To get started, you’ll need to either download a local AI model or + connect to a cloud model using an API key +

+
+
+ +

+ Setup Local Model +

+
+ } + > + +

+ Setup Remote Provider +

+ + } + >
+
+
+
+ + ) +} + +export default SetupScreen diff --git a/web-app/src/containers/dynamicControllerSetting/InputControl.tsx b/web-app/src/containers/dynamicControllerSetting/InputControl.tsx index 6645b1593..9db50548d 100644 --- a/web-app/src/containers/dynamicControllerSetting/InputControl.tsx +++ b/web-app/src/containers/dynamicControllerSetting/InputControl.tsx @@ -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 ( -
+
onChange(newValue)} /> ) diff --git a/web-app/src/mock/data.ts b/web-app/src/mock/data.ts index ff956a633..f09cb9ce3 100644 --- a/web-app/src/mock/data.ts +++ b/web-app/src/mock/data.ts @@ -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
This is blue bold text using HTML inside Markdown!
\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', - }, - }, -] diff --git a/web-app/src/routes/index.tsx b/web-app/src/routes/index.tsx index 332a45b18..afe2f7c84 100644 --- a/web-app/src/routes/index.tsx +++ b/web-app/src/routes/index.tsx @@ -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 + } return (
@@ -28,7 +43,6 @@ function Index() { {t('chat.description', { ns: 'chat' })}

-
diff --git a/web-app/src/routes/settings/providers/$providerName.tsx b/web-app/src/routes/settings/providers/$providerName.tsx index 30178bd6b..6e3062e21 100644 --- a/web-app/src/routes/settings/providers/$providerName.tsx +++ b/web-app/src/routes/settings/providers/$providerName.tsx @@ -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): { 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 ( -
- -

{t('common.settings')}

-
-
-
- -
-
-
-
-

- {getProviderTitle(providerName)} -

- { - if (provider) { - updateProvider(providerName, { ...provider, active: e }) - } - }} - /> -
- - {/* Settings */} - - {provider?.settings.map((setting, settingIndex) => { - // Use the DynamicController component - const actionComponent = ( -
- { - 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 = { - 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, - }) - } - }} - /> -
- ) - - return ( - + +
+ +

{t('common.settings')}

+
+
+
+ +
+
+
+
+

+ {getProviderTitle(providerName)} +

+ { + if (provider) { + updateProvider(providerName, { ...provider, active: e }) } - description={ - ( - - ), - p: ({ ...props }) => ( -

- ), + }} + /> +

+ + {/* Settings */} + + {provider?.settings.map((setting, settingIndex) => { + // Use the DynamicController component + const actionComponent = ( +
+ { + 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 = { + 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} - /> - ) - })} - +
+ ) - {/* Models */} - -

- Models -

-
-
- } - > - {provider?.models.map((model, modelIndex) => { - const capabilities = model.capabilities || [] - return ( - -

{model.id}

- -
- } - actions={ -
- - {model.settings && ( - - )} - -
- } - /> - ) - })} - + } + > + {provider?.models.map((model, modelIndex) => { + const capabilities = model.capabilities || [] + return ( + +

{model.id}

+ +
+ } + actions={ +
+ + {model.settings && ( + + )} + +
+ } + /> + ) + })} + +
-
+ ) }