From 1e8d5c61674d36ede6d9da5c7115fb5c970e3167 Mon Sep 17 00:00:00 2001 From: 12core1 <12533753+visnkmr@users.noreply.github.com> Date: Sun, 18 May 2025 18:26:22 +0530 Subject: [PATCH 01/44] tryout --- package.json | 5 +- src/app/page.tsx | 17 ++- src/components/gptchatinterface.tsx | 173 ++++++++++++++++++++++++---- tsconfig.json | 2 +- 4 files changed, 165 insertions(+), 32 deletions(-) diff --git a/package.json b/package.json index 1f0d2a6..fee35bf 100644 --- a/package.json +++ b/package.json @@ -88,6 +88,7 @@ "fastest-levenshtein": "^1.0.16", "fuse.js": "^7.0.0", "html-react-parser": "^5.1.16", + "js-big-decimal": "^2.2.0", "js-file-download": "^0.4.12", "next-optimized-images": "3.0.0-canary.10", "next-pwa": "^5.6.0", @@ -99,9 +100,9 @@ "react-toastify": "^9.1.3", "react-wrap-balancer": "^1.1.1", "react-zoom-pan-pinch": "^3.6.1", + "sharp": "0.30.7", "simple-peer": "^9.11.1", - "toast-notification-js": "^1.1.0", - "sharp": "0.30.7" + "toast-notification-js": "^1.1.0" }, "resolutions": { "sharp": "0.30.7" diff --git a/src/app/page.tsx b/src/app/page.tsx index 81bea08..b81633d 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,12 +1,17 @@ +"use client" + // ... import React from 'react' import Greet from '../components/greet' +import GPTchatinterface from '../components/gptchatinterface' export default function Home() { - - return ( -
- -
- ) + let url=typeof window !== 'undefined' ? window.location.hostname : '/' + console.log(url) + return + // return ( + //
+ // + //
+ // ) } \ No newline at end of file diff --git a/src/components/gptchatinterface.tsx b/src/components/gptchatinterface.tsx index 7357879..1e3905c 100644 --- a/src/components/gptchatinterface.tsx +++ b/src/components/gptchatinterface.tsx @@ -13,7 +13,7 @@ import {fetchEventSource} from '@microsoft/fetch-event-source'; import { Checkbox } from "./ui/checkbox"; import { Markdown } from "./markdown"; import { useDebounce } from "use-debounce"; - +import bigDecimal from 'js-big-decimal' // import MyComponent from "./route"; interface gptargs{ message?:FileItem, @@ -33,8 +33,94 @@ function getchattime(){ function getchattimestamp(){ return new Date().getTime() } +interface ModelRow { + model: string + cost: { + prompt_token: number + completion_token: number + } +} +const supportedProviderList = [ + 'openai', + 'anthropic', + 'google', + 'deepseek', + 'perplexity', + 'cohere', + 'mistralai', + 'meta-llama', +] export default function GPTchatinterface({message,fgptendpoint="localhost",setasollama=false}:gptargs){ - // let sao=(value:boolean)=>{setasollama=value}; + useEffect(() => { + const fetchModels = async () => { + try { + const res = await fetch('https://openrouter.ai/api/v1/models', {}); + if (!res.ok) { + throw new Error(`Failed to fetch models: ${res.status} ${res.statusText}`); + } + + let data; + try { + data = await res.json(); + } catch (e) { + throw new Error('Failed to parse API response as JSON'); + } + + console.log(data.data); + const models = data.data + + // // Create main directory + // if (!fs.existsSync(PATH_TO_PROVIDERS)) { + // fs.mkdirSync(PATH_TO_PROVIDERS) + // } + + // Group models by provider + const providerModels = new Map() + + for (const model of models) { + if (!model?.id || !model?.pricing?.prompt || !model?.pricing?.completion ) { + console.warn('Skipping invalid model:', model) + continue + } + const [provider, ...modelParts] = model.id.split('/') + // if (!supportedProviderList.includes(provider)) { + // continue + // } + if (!providerModels.has(provider)) { + providerModels.set(provider, []) + } + + // Convert pricing values to numbers before using toFixed(10) + const promptPrice = new bigDecimal(model.pricing.prompt).getValue() + const completionPrice = new bigDecimal(model.pricing.completion).getValue() + + const modelRow: ModelRow = { + model: modelParts.join('/'), // Only include the part after the provider + cost: { + prompt_token: parseFloat(promptPrice), + completion_token: parseFloat(completionPrice), + }, + } + + providerModels.get(provider)!.push(modelRow) + } + + const allProviders = Array.from(providerModels.values()).flat() + + // Sort by model name for easier diffs + const freemodels=allProviders.filter((m)=>{return m.cost.prompt_token<=0?true:false}).sort((a, b) => a.model.localeCompare(b.model)) + console.log(freemodels) + } catch (error) { + console.error('Error fetching models:', error); + } + }; + + + + fetchModels(); + }, []); + + // let sao=(value:boolean)=>{setasollama=value}; const [isollama,sao]=useState(setasollama) // const [useollama,seto]=useState(setasollama) @@ -116,7 +202,8 @@ export default function GPTchatinterface({message,fgptendpoint="localhost",setas if(question.toLocaleLowerCase().startsWith("o2c") ||!filedimegptisrunning){ //outside of current context -o2c const requestBody = { - "model": "lmstudio-community/deepseek-r1-distill-qwen-7b", + "model": "nousresearch/deephermes-3-mistral-24b-preview:free", + // "model": "lmstudio-community/deepseek-r1-distill-qwen-7b", "messages": [ // {"role": "system", "content": "Always answer in rhymes."}, {"role": "user", "content":question.replace("o2c", "")} @@ -125,11 +212,20 @@ if(question.toLocaleLowerCase().startsWith("o2c") ||!filedimegptisrunning){ //ou }; // let tempstore=useRef([]) // Fetch the stream from the Ollama API - fetch(`http://${fgptendpoint}:11434/v1/chat/completions`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, + fetch( + "https://openrouter.ai/api/v1/chat/completions", { + method: "POST", + headers: { + "Authorization": "Bearer ", + // "HTTP-Referer": "", // Optional. Site URL for rankings on openrouter.ai. + // "X-Title": "", // Optional. Site title for rankings on openrouter.ai. + "Content-Type": "application/json" + }, + // `http://${fgptendpoint}:11434/v1/chat/completions`, { + // method: 'POST', + // headers: { + // 'Content-Type': 'application/json' + // }, body: JSON.stringify(requestBody) }) @@ -139,28 +235,59 @@ if(question.toLocaleLowerCase().startsWith("o2c") ||!filedimegptisrunning){ //ou const decoder = new TextDecoder('utf-8'); return reader.read().then(function processChunk({ done, value }) { - const chunk = decoder.decode(value).slice(5); - if (done || chunk.includes("[DONE]")) { + console.log("cal:----?>"+decoder.decode(value)) + const chunk = decoder.decode(value); + chunk + // Filter out the "OPENROUTER PROCESSING" chunks if using openrouter + .replaceAll(": OPENROUTER PROCESSING", "") + .split("data: ") + .filter((l: string) => l.trim()) + .map((line: string) => { + if (done || line.includes("[DONE]")) { console.log('Stream complete'); + done=true; return; } + try { + const choice = JSON.parse(line.trim()).choices[0]; + const resp = "delta" in choice ? choice.delta.content : choice.text; + setmessage((old)=>{ + let dm=old+resp; + return dm}); + // if (content) output.completeChunks.push(content); + } catch (e) { + console.log(e) + } + }); + if (done ) { + console.log('Stream complete'); + done=true; + return; + } + // if (done || chunk.includes("[DONE]")) { + // console.log('Stream complete'); + // return; + // } + // Decode the chunk and log it // console.log(JSON.parse(chunk)); // if(JSON.parse(chunk)){ - try{ - - let resp=JSON.parse(chunk); - resp=resp.choices[0].delta.content; - console.log(resp) - setmessage((old)=>{ - let dm=old+resp; - return dm}); - // } - } - catch (error) { - console.error(error) - } + // try{ + // console.log(chunk) + // let resp=JSON.stringify((chunk)); + // resp=resp+"\n"; + // // let resp=JSON.parse((chunk)); + // // resp=resp.choices[0].delta.content; + // // console.log(resp) + // setmessage((old)=>{ + // let dm=old+resp; + // return dm}); + // // } + // } + // catch (error) { + // console.error(error) + // } // Read the next chunk return reader.read().then(processChunk); }); diff --git a/tsconfig.json b/tsconfig.json index c89e329..6095b9d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -48,7 +48,7 @@ // Do not generate custom helper functions like __extends in compiled output // "module": "NodeNext", // Enable all strict type checking options - "strict": true, + "strict": false, // Raise an error on expressions and declarations with an inferred type of any "noImplicitAny": true, // Raise an error on this expressions with an inferred type of any From b4bc869b432c26c3fd5c6e48896cababd744e598 Mon Sep 17 00:00:00 2001 From: 12core1 <12533753+visnkmr@users.noreply.github.com> Date: Tue, 27 May 2025 20:08:53 +0530 Subject: [PATCH 02/44] update versions of libs --- next-env.d.ts | 2 +- next.config.js | 2 ++ package.json | 75 ++++++++++++++++++++------------------- src-tauri/tauri.conf.json | 2 +- src/app/layout.tsx | 3 ++ 5 files changed, 45 insertions(+), 39 deletions(-) diff --git a/next-env.d.ts b/next-env.d.ts index fd36f94..3cd7048 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -3,4 +3,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/next.config.js b/next.config.js index 820c4bf..30ef55d 100644 --- a/next.config.js +++ b/next.config.js @@ -22,10 +22,12 @@ const nextConfig = // withPWA( // withBundleAnalyzer( { + reactStrictMode: false, // withPWA, // withOptimizedImages, experimental: { + reactCompiler:true, // appDir: true, // optimizeCss: true, esmExternals: true, diff --git a/package.json b/package.json index fee35bf..fbb1210 100644 --- a/package.json +++ b/package.json @@ -17,24 +17,24 @@ "license": "MIT", "devDependencies": { "@radix-ui/react-popover": "1.0.7", - "@swc/core": "^1.7.26", + "@swc/core": "^1.11.29", "@tailwindcss/line-clamp": "^0.4.4", - "@tanstack/react-query": "^5.56.2", - "@tanstack/react-query-devtools": "^5.58.0", - "@tanstack/react-table": "^8.20.5", - "@tauri-apps/cli": "^1.6.2", - "@types/classnames": "^2.3.1", - "@types/lodash": "^4.17.9", + "@tanstack/react-query": "^5.77.2", + "@tanstack/react-query-devtools": "^5.77.2", + "@tanstack/react-table": "^8.21.3", + "@tauri-apps/cli": "^1.6.3", + "@types/classnames": "^2.3.4", + "@types/lodash": "^4.17.17", "@types/lodash.debounce": "^4.0.9", "@types/lodash.take": "^4.1.9", - "@types/luxon": "^3.4.2", - "@types/node": "^20.16.10", - "@types/react": "^18.3.10", + "@types/luxon": "^3.6.2", + "@types/node": "^20.17.50", + "@types/react": "^18.3.23", "@types/react-lazy-load-image-component": "^1.6.4", - "@types/react-virtualized": "^9.21.30", + "@types/react-virtualized": "^9.22.2", "@types/simple-peer": "^9.11.8", - "autoprefixer": "^10.4.20", - "axios": "^1.7.7", + "autoprefixer": "^10.4.21", + "axios": "^1.9.0", "classnames": "^2.5.1", "clean-css": "^5.3.3", "clsx": "^2.1.1", @@ -47,59 +47,60 @@ "lodash.debounce": "^4.0.8", "lodash.take": "^4.1.1", "lucide-react": "^0.295.0", - "luxon": "^3.5.0", - "next": "^14.2.13", + "luxon": "^3.6.1", + "next": "^15", "next-sitemap": "^4.2.3", "next-themes": "^0.2.1", - "radix-ui": "^1.0.1", + "radix-ui": "^1.4.2", "react": "^18.3.1", "react-countup": "^6.5.3", "react-dom": "^18.3.1", "react-query": "^3.39.3", "react-text-transition": "^3.1.0", - "react-virtualized": "^9.22.5", - "swr": "^2.2.5", + "react-virtualized": "^9.22.6", + "swr": "^2.3.3", "tailwind": "^4.0.0", - "tailwind-merge": "^2.5.2", - "tailwindcss": "^3.4.13", - "terser-webpack-plugin": "^5.3.10", - "typescript": "^5.6.2", + "tailwind-merge": "^2.6.0", + "tailwindcss": "^3.4.17", + "terser-webpack-plugin": "^5.3.14", + "typescript": "^5.8.3", "remark-breaks": "^3.0.3", "remark-gfm": "^3.0.1", "remark-math": "^5.1.1", "rehype-highlight": "^6.0.0", "react-markdown": "^8.0.7", "use-debounce": "^9.0.4", - "mermaid": "^10.9.1" + "mermaid": "^10.9.3" }, "sideEffects": false, "dependencies": { "@microsoft/fetch-event-source": "^2.0.1", - "@next/bundle-analyzer": "^14.2.13", - "@radix-ui/react-alert-dialog": "^1.1.1", - "@radix-ui/react-context-menu": "^2.2.1", - "@radix-ui/react-label": "^2.1.0", - "@radix-ui/react-progress": "^1.1.0", - "@radix-ui/react-slot": "^1.1.0", - "@radix-ui/react-switch": "^1.1.0", - "@radix-ui/react-toast": "^1.2.1", + "@next/bundle-analyzer": "^14.2.29", + "@radix-ui/react-alert-dialog": "^1.1.14", + "@radix-ui/react-context-menu": "^2.2.15", + "@radix-ui/react-label": "^2.1.7", + "@radix-ui/react-progress": "^1.1.7", + "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-switch": "^1.2.5", + "@radix-ui/react-toast": "^1.2.14", "@tauri-apps/api": "^1.6.0", - "class-variance-authority": "^0.7.0", + "babel-plugin-react-compiler": "^19.1.0-rc.2", + "class-variance-authority": "^0.7.1", "fastest-levenshtein": "^1.0.16", - "fuse.js": "^7.0.0", - "html-react-parser": "^5.1.16", "js-big-decimal": "^2.2.0", + "fuse.js": "^7.1.0", + "html-react-parser": "^5.2.5", "js-file-download": "^0.4.12", "next-optimized-images": "3.0.0-canary.10", "next-pwa": "^5.6.0", "react-fast-marquee": "^1.6.5", "react-hooks-global-state": "^2.1.0", - "react-hot-toast": "^2.4.1", - "react-lazy-load-image-component": "^1.6.2", + "react-hot-toast": "^2.5.2", + "react-lazy-load-image-component": "^1.6.3", "react-resizable-panels": "^1.0.10", "react-toastify": "^9.1.3", "react-wrap-balancer": "^1.1.1", - "react-zoom-pan-pinch": "^3.6.1", + "react-zoom-pan-pinch": "^3.7.0", "sharp": "0.30.7", "simple-peer": "^9.11.1", "toast-notification-js": "^1.1.0" diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index ad7a957..207bfc2 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -55,7 +55,7 @@ }, "windows": [ { - "fullscreen": false, + "fullscreen": true, "maximized": true, "resizable": true, "title": "Filedime", diff --git a/src/app/layout.tsx b/src/app/layout.tsx index e5c38cc..55408f3 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -26,6 +26,9 @@ export default function RootLayout({ // const [showon, setshow] = useLocalStorage("dark",true); return ( + + + + + `) + printWindow.document.close() + } + } + + const downloadFile = (content: string, fileName: string, contentType: string) => { + const blob = new Blob([content], { type: contentType }) + const url = URL.createObjectURL(blob) + const a = document.createElement("a") + a.href = url + a.download = fileName + document.body.appendChild(a) + a.click() + document.body.removeChild(a) + URL.revokeObjectURL(url) + } + + return ( + !open && onClose()}> + + + Export Chat + + +
+ setExportFormat(value as ExportFormat)}> +
+ + +
+
+ + +
+
+ + +
+
+
+ +
+ + +
+
+
+ ) +} diff --git a/src/components/batu/components/filegpt-url.tsx b/src/components/batu/components/filegpt-url.tsx new file mode 100644 index 0000000..c4e4dc6 --- /dev/null +++ b/src/components/batu/components/filegpt-url.tsx @@ -0,0 +1,46 @@ +"use client" + +import { useEffect, useState } from "react" +import { Input } from "./ui/input" +import { Label } from "./ui/label" +import { Button } from "./ui/button" +import { SaveIcon } from "lucide-react" + +interface FileGPTUrlInputProps { + filegpturl: string + setFilegpturl: (key: string) => void +} + +export default function FileGPTUrl({ filegpturl, setFilegpturl }: FileGPTUrlInputProps) { + const [inputValue, setInputValue] = useState(filegpturl) + + useEffect(() => { + setInputValue(filegpturl) + }, [filegpturl]) + + const handleSave = () => { + setFilegpturl(inputValue) + localStorage.setItem("filegpt_url", inputValue) + } + + return ( +
+ +
+
+ setInputValue(e.target.value)} + placeholder="FileGPT Endpoint (e.g., http://localhost:8694)" + className="pr-10" + /> +
+ +
+
+ ) +} diff --git a/src/components/batu/components/lmstudio-url.tsx b/src/components/batu/components/lmstudio-url.tsx new file mode 100644 index 0000000..ad42752 --- /dev/null +++ b/src/components/batu/components/lmstudio-url.tsx @@ -0,0 +1,46 @@ +"use client" + +import { useEffect, useState } from "react" +import { Input } from "../components/ui/input" +import { Label } from "../components/ui/label" +import { Button } from "../components/ui/button" +import { EyeIcon, EyeOffIcon, SaveIcon } from "lucide-react" + +interface LMUrlInputProps { + lmurl: string + setlmurl: (key: string) => void +} + +export default function LMStudioURL({ lmurl, setlmurl }: LMUrlInputProps) { + // const [showKey, setShowKey] = useState(false) + const [inputValue, setInputValue] = useState(lmurl) + useEffect(()=>{ + setInputValue(lmurl) + },[lmurl]) + + const handleSave = () => { + setlmurl(inputValue) + localStorage.setItem("lmstudio_url", inputValue) + } + + return ( +
+ +
+
+ setInputValue(e.target.value)} + placeholder="LM Studio IP" + className="pr-10" + /> +
+ +
+
+ ) +} diff --git a/src/components/batu/components/localmodelname.tsx b/src/components/batu/components/localmodelname.tsx new file mode 100644 index 0000000..7d34fee --- /dev/null +++ b/src/components/batu/components/localmodelname.tsx @@ -0,0 +1,45 @@ +"use client" + +import { useEffect, useState } from "react" +import { Input } from "../components/ui/input" +import { Label } from "../components/ui/label" +import { Button } from "../components/ui/button" +import { EyeIcon, EyeOffIcon, SaveIcon } from "lucide-react" + +interface LMmodelnameProps { + model_name: string + set_model_name: (key: string) => void +} + +export default function LMStudioModelName({ model_name, set_model_name }: LMmodelnameProps) { + // const [showKey, setShowKey] = useState(false) + const [inputValue, setInputValue] = useState(model_name) + useEffect(()=>{ + setInputValue(model_name) + },[model_name]) + const handleSave = () => { + set_model_name(inputValue) + localStorage.setItem("lmstudio_model_name", inputValue) + } + + return ( +
+ +
+
+ setInputValue(e.target.value)} + placeholder="LM Studio Model name" + className="pr-10" + /> +
+ +
+
+ ) +} diff --git a/src/components/batu/components/markdown-renderer.tsx b/src/components/batu/components/markdown-renderer.tsx new file mode 100644 index 0000000..9217de6 --- /dev/null +++ b/src/components/batu/components/markdown-renderer.tsx @@ -0,0 +1,33 @@ +'use client'; + +import React from 'react'; +import { ScrollArea } from '../components/ui/scroll-area'; +import { Markdown } from './markdown'; +import { cn } from '../lib/utils'; + +interface MarkdownRendererProps { + content: string; + className?: string; +} + +/** + * Renderer for Markdown content with scrollable container + */ +export function MarkdownRenderer({ + content, + className +}: MarkdownRendererProps) { + return ( +
+ +
+ + {content} + +
+
+
+ ); +} \ No newline at end of file diff --git a/src/components/batu/components/markdown.tsx b/src/components/batu/components/markdown.tsx new file mode 100644 index 0000000..ba84a69 --- /dev/null +++ b/src/components/batu/components/markdown.tsx @@ -0,0 +1,209 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { cn } from '../lib/utils'; +import { marked } from 'marked'; +import { memo, useId, useMemo } from 'react'; +import ReactMarkdown, { Components } from 'react-markdown'; +import remarkGfm from 'remark-gfm'; +import { CodeBlock, CodeBlockCode } from './codeblock'; +import React from 'react'; + +export type MarkdownProps = { + children: string; + id?: string; + className?: string; + components?: Partial; +}; + +function parseMarkdownIntoBlocks(markdown: string): string[] { + const tokens = marked.lexer(markdown); + return tokens.map((token: any) => token.raw); +} + +function extractLanguage(className?: string): string { + if (!className) return 'plaintext'; + const match = className.match(/language-(\w+)/); + return match ? match[1] : 'plaintext'; +} + +const INITIAL_COMPONENTS: Partial = { + code: function CodeComponent({ className, children, ...props }: any) { + const isInline = + !props.node?.position?.start.line || + props.node?.position?.start.line === props.node?.position?.end.line; + + if (isInline) { + return ( + + {children} + + ); + } + + const language = extractLanguage(className); + + return ( + + + + ); + }, + pre: function PreComponent({ children }: any) { + return <>{children}; + }, + ul: function UnorderedList({ children, ...props }: any) { + return ( +
    + {children} +
+ ); + }, + ol: function OrderedList({ children, ...props }: any) { + return ( +
    + {children} +
+ ); + }, + li: function ListItem({ children, ...props }: any) { + return ( +
  • + {children} +
  • + ); + }, + h1: function H1({ children, ...props }: any) { + return ( +

    + {children} +

    + ); + }, + h2: function H2({ children, ...props }: any) { + return ( +

    + {children} +

    + ); + }, + h3: function H3({ children, ...props }: any) { + return ( +

    + {children} +

    + ); + }, + blockquote: function Blockquote({ children, ...props }: any) { + return ( +
    + {children} +
    + ); + }, + a: function Anchor({ children, href, ...props }: any) { + return ( + + {children} + + ); + }, + table: function Table({ children, ...props }: any) { + return ( + + {children} +
    + ); + }, + th: function TableHeader({ children, ...props }: any) { + return ( + + {children} + + ); + }, + td: function TableCell({ children, ...props }: any) { + return ( + + {children} + + ); + }, +}; + +const MemoizedMarkdownBlock = memo( + function MarkdownBlock({ + content, + components = INITIAL_COMPONENTS, + }: { + content: string; + components?: Partial; + }) { + return ( + + {content} + + ); + }, + function propsAreEqual(prevProps: any, nextProps: any) { + return prevProps.content === nextProps.content; + }, +); + +MemoizedMarkdownBlock.displayName = 'MemoizedMarkdownBlock'; + +function MarkdownComponent({ + children, + id, + className, + components = INITIAL_COMPONENTS, +}: MarkdownProps) { + const generatedId = useId(); + const blockId = id ?? generatedId; + const blocks = useMemo(() => parseMarkdownIntoBlocks(children), [children]); + + return ( +
    + {blocks.map((block, index) => ( + + ))} +
    + ); +} + +const Markdown = memo(MarkdownComponent); +Markdown.displayName = 'Markdown'; + +export { Markdown }; \ No newline at end of file diff --git a/src/components/batu/components/message-item.tsx b/src/components/batu/components/message-item.tsx new file mode 100644 index 0000000..95ce4a8 --- /dev/null +++ b/src/components/batu/components/message-item.tsx @@ -0,0 +1,162 @@ +"use client" + +import type { Message } from "../lib/types" +import { UserIcon, BotIcon, CopyIcon, GitBranchIcon, RefreshCw } from "lucide-react" +import { cn } from "../lib/utils" +import { format } from "date-fns" +// import ReactMarkdown from "react-markdown" +import {Markdown} from "./markdown" +// import { Prism as SyntaxHighlighter } from "react-syntax-highlighter" +// import { vscDarkPlus } from "react-syntax-highlighter/dist/esm/styles/prism" +import { useEffect, useState } from "react" +import { Button } from "../components/ui/button" + +interface MessageItemProps { + message: Message + isStreaming?: boolean + onCopy: () => void + onBranch: () => void + setdsm: any + setmts: any +} + +export default function MessageItem({ message, isStreaming = false, onCopy, onBranch,setdsm,setmts }: MessageItemProps) { + const isUser = message.role === "user" + const [showCursor, setShowCursor] = useState(true) + const [isHovered, setIsHovered] = useState(false) + + // Blinking cursor effect for streaming messages + useEffect(() => { + if (!isStreaming) return + + const interval = setInterval(() => { + setShowCursor((prev) => !prev) + }, 500) + + return () => clearInterval(interval) + }, [isStreaming]) + + // Custom renderer for code blocks + // const components = { + // code({ node, inline, className, children, ...props }: any) { + // const match = /language-(\w+)/.exec(className || "") + // return !inline && match ? ( + // + // {String(children).replace(/\n$/, "")} + // + // ) : ( + // + // {children} + // + // ) + // }, + // } + const Resend=()=>{ + setdsm(true) + setmts(message.content) + } + + return ( +
    setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)}> +
    +
    + {/*
    + {isUser ? ( +
    + +
    + ) : ( +
    + +
    + )} +
    */} + +
    +
    + + {/* {isUser ? "You" : "AI Assistant"} */} + {message.model && !isUser && ( + ({getModelDisplayName(message.model)}) + )} + + {/* + {format(new Date(message.timestamp), "MMM d, h:mm a")} + */} +
    + +
    + {message.content} + {isStreaming && showCursor && } +
    +
    + + +
    + {!isStreaming && ( +
    + + + +
    + )} +
    +
    + ) +} + +// Helper function to get a consistent color based on model name +function getModelColor(modelId: string): string { + const colors = [ + "purple-500", + "pink-500", + "rose-500", + "red-500", + "orange-500", + "amber-500", + "yellow-500", + "lime-500", + "green-500", + "emerald-500", + "teal-500", + "cyan-500", + "sky-500", + "blue-500", + "indigo-500", + "violet-500", + ] + + // Simple hash function to get consistent color + let hash = 0 + for (let i = 0; i < modelId.length; i++) { + hash = modelId.charCodeAt(i) + ((hash << 5) - hash) + } + + const index = Math.abs(hash) % colors.length + return colors[index] +} + +// Helper function to get a display name from model ID +function getModelDisplayName(modelId: string): string { + // Extract the model name from the provider/model format + const parts = modelId.split("/") + return parts.length > 1 ? parts[1] : modelId +} diff --git a/src/components/batu/components/model-selection-dialog.tsx b/src/components/batu/components/model-selection-dialog.tsx new file mode 100644 index 0000000..2fe0485 --- /dev/null +++ b/src/components/batu/components/model-selection-dialog.tsx @@ -0,0 +1,238 @@ +"use client" + +import { useState, useEffect } from "react" +import { Dialog, DialogContent, DialogHeader, DialogTitle } from "../components/ui/dialog" +import { BotIcon, Loader2 } from "lucide-react" +import { cn } from "../lib/utils" +import { ScrollArea } from "../components/ui/scroll-area" +import { Input } from "../components/ui/input" +import { Badge } from "../components/ui/badge" +import Marquee from "react-fast-marquee"; +interface ModelSelectionDialogProps { + isOpen: boolean + onClose: () => void + models: any[] + selectedModel: string + onSelectModel: (modelId: string) => void + apiKey: string +} + +export default function ModelSelectionDialog({ + isOpen, + onClose, + models, + selectedModel, + onSelectModel, + apiKey, +}: ModelSelectionDialogProps) { + const [isLoading, setIsLoading] = useState(false) + const [filteredModels, setFilteredModels] = useState([]) + const [searchQuery, setSearchQuery] = useState("") + + // useEffect(() => { + // if (!apiKey || models.length > 0) return + + // const fetchModels = async () => { + // setIsLoading(true) + // try { + // const response = await fetch("https://openrouter.ai/api/v1/models", { + // headers: { + // Authorization: `Bearer ${apiKey}`, + // }, + // }) + + // if (!response.ok) { + // throw new Error("Failed to fetch models") + // } + + // const data = await response.json() + // // Models are fetched in the parent component + // } catch (err) { + // console.error("Error fetching models:", err) + // } finally { + // setIsLoading(false) + // } + // } + + // fetchModels() + // }, [apiKey, models.length]) + + // Filter models based on search query + useEffect(() => { + if (!models) { + setFilteredModels([]) + return + } + + if (!searchQuery) { + setFilteredModels(models) + return + } + + const filtered = models.filter((model) => { + const modelName = model.id.toLowerCase() + const provider = model.id.split("/")[0].toLowerCase() + const modelId = model.id.split("/").pop().toLowerCase() + return ( + modelName.includes(searchQuery.toLowerCase()) || + provider.includes(searchQuery.toLowerCase()) || + modelId.includes(searchQuery.toLowerCase()) + ) + }) + + setFilteredModels(filtered) + }, [models, searchQuery]) + + const HoverMarqueeItem = ({ text }: { text: string }) => { + const [isHovered, setIsHovered] = useState(false); + + return ( +
    setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + className="text-center w-full" + > + + {text} + +
    + ); + }; + + // Get a random color for the model icon + const getModelColor = (modelId: string): string => { + const colors = [ + "bg-purple-500", + "bg-pink-500", + "bg-rose-500", + "bg-red-500", + "bg-orange-500", + "bg-amber-500", + "bg-yellow-500", + "bg-lime-500", + "bg-green-500", + "bg-emerald-500", + "bg-teal-500", + "bg-cyan-500", + "bg-sky-500", + "bg-blue-500", + "bg-indigo-500", + "bg-violet-500", + ] + + // Simple hash function to get consistent color + let hash = 0 + for (let i = 0; i < modelId.length; i++) { + hash = modelId.charCodeAt(i) + ((hash << 5) - hash) + } + + const index = Math.abs(hash) % colors.length + return colors[index] + } + const [modelcount, setmodelcount] = useState(10) + + // Format pricing to be more readable + // const formatPrice = (price: string) => { + // if (!price) return "N/A" + // const numPrice = Number.parseFloat(price) + // if (numPrice === 0) return "Free" + // return `$${numPrice.toFixed(7)}` + // } + // const [isHovered, setIsHovered] = useState(false); + return ( + !open && onClose()}> + + + Select a Model + + +
    + setSearchQuery(e.target.value)} + className="w-full" + /> +
    + + {isLoading ? ( +
    + +
    + ) : ( + // +
    + {filteredModels.slice(0, modelcount).map((model) => ( +
    onSelectModel(model.id)} + > +
    + +
    + +
    + +

    + {model.id.split("/")[0]} +

    +
    + +
    + + {model.context_length.toLocaleString()} tokens + + + {/*
    + Input: + {formatPrice(model.pricing?.prompt)} +
    + +
    + Output: + {formatPrice(model.pricing?.completion)} +
    */} +
    +
    + ))} + {modelcount+10 < filteredModels.length && (
    setmodelcount( modelcount+10)} + > + + +
    +

    + More models +

    +
    + + +
    )} +
    + //
    + )} +
    +
    + ) +} diff --git a/src/components/batu/components/model-selector.tsx b/src/components/batu/components/model-selector.tsx new file mode 100644 index 0000000..a7989a9 --- /dev/null +++ b/src/components/batu/components/model-selector.tsx @@ -0,0 +1,98 @@ +"use client" + +import { useEffect, useState } from "react" +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../components/ui/select" +import { Label } from "../components/ui/label" +import { Loader2 } from "lucide-react" +// import type { Model } from "../lib/types" + +interface ModelSelectorProps { + apiKey: string + selectedModel: string + setSelectedModel: (model: string) => void +} + +export default function ModelSelector({ apiKey, selectedModel, setSelectedModel }: ModelSelectorProps) { + const [models, setModels] = useState([]) + const [isLoading, setIsLoading] = useState(false) + const [error, setError] = useState(null) + + useEffect(() => { + if (!apiKey) return + + const fetchModels = async () => { + setIsLoading(true) + setError(null) + + try { + const response = await fetch("https://openrouter.ai/api/v1/models", { + headers: { + Authorization: `Bearer ${apiKey}`, + }, + }) + + if (!response.ok) { + throw new Error("Failed to fetch models") + } + + const data = await response.json() + + // Filter for free models (where pricing is 0) + const freeModels = data.data.filter((model: any) => { + return Number.parseFloat(model.pricing?.prompt) <= 0 && Number.parseFloat(model.pricing?.completion) <= 0 + }) + + setModels( + freeModels.map((model: any) => ({ + id: model.id, + name: model.name || model.id.split("/").pop(), + provider: model.id.split("/")[0], + })), + ) + + // Set the first model as selected if none is selected + if (freeModels.length > 0 && !selectedModel) { + setSelectedModel(freeModels[0].id) + } + } catch (err) { + console.error("Error fetching models:", err) + setError(err instanceof Error ? err.message : "Failed to fetch models") + } finally { + setIsLoading(false) + } + } + + fetchModels() + }, [apiKey, selectedModel, setSelectedModel]) + + return ( +
    + + +
    + ) +} diff --git a/src/components/batu/components/theme-provider.tsx b/src/components/batu/components/theme-provider.tsx new file mode 100644 index 0000000..55c2f6e --- /dev/null +++ b/src/components/batu/components/theme-provider.tsx @@ -0,0 +1,11 @@ +'use client' + +import * as React from 'react' +import { + ThemeProvider as NextThemesProvider, + type ThemeProviderProps, +} from 'next-themes' + +export function ThemeProvider({ children, ...props }: ThemeProviderProps) { + return {children} +} diff --git a/src/components/batu/components/ui/accordion.tsx b/src/components/batu/components/ui/accordion.tsx new file mode 100644 index 0000000..1fc96f6 --- /dev/null +++ b/src/components/batu/components/ui/accordion.tsx @@ -0,0 +1,58 @@ +"use client" + +import * as React from "react" +import * as AccordionPrimitive from "@radix-ui/react-accordion" +import { ChevronDown } from "lucide-react" + +import { cn } from "../../lib/utils" + +const Accordion = AccordionPrimitive.Root + +const AccordionItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AccordionItem.displayName = "AccordionItem" + +const AccordionTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + svg]:rotate-180", + className + )} + {...props} + > + {children} + + + +)) +AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName + +const AccordionContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + +
    {children}
    +
    +)) + +AccordionContent.displayName = AccordionPrimitive.Content.displayName + +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } diff --git a/src/components/batu/components/ui/alert-dialog.tsx b/src/components/batu/components/ui/alert-dialog.tsx new file mode 100644 index 0000000..58c23d1 --- /dev/null +++ b/src/components/batu/components/ui/alert-dialog.tsx @@ -0,0 +1,141 @@ +"use client" + +import * as React from "react" +import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" + +import { cn } from "../../lib/utils" +import { buttonVariants } from "../../components/ui/button" + +const AlertDialog = AlertDialogPrimitive.Root + +const AlertDialogTrigger = AlertDialogPrimitive.Trigger + +const AlertDialogPortal = AlertDialogPrimitive.Portal + +const AlertDialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName + +const AlertDialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + +)) +AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName + +const AlertDialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
    +) +AlertDialogHeader.displayName = "AlertDialogHeader" + +const AlertDialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
    +) +AlertDialogFooter.displayName = "AlertDialogFooter" + +const AlertDialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName + +const AlertDialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogDescription.displayName = + AlertDialogPrimitive.Description.displayName + +const AlertDialogAction = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName + +const AlertDialogCancel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName + +export { + AlertDialog, + AlertDialogPortal, + AlertDialogOverlay, + AlertDialogTrigger, + AlertDialogContent, + AlertDialogHeader, + AlertDialogFooter, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogAction, + AlertDialogCancel, +} diff --git a/src/components/batu/components/ui/alert.tsx b/src/components/batu/components/ui/alert.tsx new file mode 100644 index 0000000..e9286a3 --- /dev/null +++ b/src/components/batu/components/ui/alert.tsx @@ -0,0 +1,59 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "../../lib/utils" + +const alertVariants = cva( + "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground", + { + variants: { + variant: { + default: "bg-background text-foreground", + destructive: + "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +const Alert = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes & VariantProps +>(({ className, variant, ...props }, ref) => ( +
    +)) +Alert.displayName = "Alert" + +const AlertTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
    +)) +AlertTitle.displayName = "AlertTitle" + +const AlertDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
    +)) +AlertDescription.displayName = "AlertDescription" + +export { Alert, AlertTitle, AlertDescription } diff --git a/src/components/batu/components/ui/aspect-ratio.tsx b/src/components/batu/components/ui/aspect-ratio.tsx new file mode 100644 index 0000000..d6a5226 --- /dev/null +++ b/src/components/batu/components/ui/aspect-ratio.tsx @@ -0,0 +1,7 @@ +"use client" + +import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio" + +const AspectRatio = AspectRatioPrimitive.Root + +export { AspectRatio } diff --git a/src/components/batu/components/ui/avatar.tsx b/src/components/batu/components/ui/avatar.tsx new file mode 100644 index 0000000..9122baa --- /dev/null +++ b/src/components/batu/components/ui/avatar.tsx @@ -0,0 +1,50 @@ +"use client" + +import * as React from "react" +import * as AvatarPrimitive from "@radix-ui/react-avatar" + +import { cn } from "../../lib/utils" + +const Avatar = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +Avatar.displayName = AvatarPrimitive.Root.displayName + +const AvatarImage = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AvatarImage.displayName = AvatarPrimitive.Image.displayName + +const AvatarFallback = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName + +export { Avatar, AvatarImage, AvatarFallback } diff --git a/src/components/batu/components/ui/badge.tsx b/src/components/batu/components/ui/badge.tsx new file mode 100644 index 0000000..c2466e3 --- /dev/null +++ b/src/components/batu/components/ui/badge.tsx @@ -0,0 +1,29 @@ +import type * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "../../lib/utils" + +const badgeVariants = cva( + "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", + { + variants: { + variant: { + default: "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", + secondary: "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", + destructive: "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", + outline: "text-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + }, +) + +export interface BadgeProps extends React.HTMLAttributes, VariantProps {} + +function Badge({ className, variant, ...props }: BadgeProps) { + return
    +} + +export { Badge, badgeVariants } diff --git a/src/components/batu/components/ui/breadcrumb.tsx b/src/components/batu/components/ui/breadcrumb.tsx new file mode 100644 index 0000000..c1720a5 --- /dev/null +++ b/src/components/batu/components/ui/breadcrumb.tsx @@ -0,0 +1,115 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { ChevronRight, MoreHorizontal } from "lucide-react" + +import { cn } from "../../lib/utils" + +const Breadcrumb = React.forwardRef< + HTMLElement, + React.ComponentPropsWithoutRef<"nav"> & { + separator?: React.ReactNode + } +>(({ ...props }, ref) =>