* chore: initial new FE setup * chore: update namespace text-left-panel foreground variable * chore: enable dynamic mainview color * chore: remove greetings new chat * chore: fix chat input style * chore: simplify hook useAppearance * chore: enable internationalization * chore: prepare vn locale * chore: keyboardshortcut layout * chore: update keyboard shortcut exclude pathname * chore: update state active setting route * chore: fix update theme by system * chore: handle dynamic primary color * chore: fix left panel navigation active state and styled item privacy analytic * chore: reorder general setting being a first * chore: add function reset appearance * chore: update scrollbar * chore: update delete thread with dialog confirmation * chore: update state dialog inside dropdown menu * chore: wip thread detail or chat page * chore: wip model dropdown * chore: prepare model dropdown select * chore: update model providers setting * chore: show provider on model dropdown based isActive toogle * chore: update layout model provider * chore: update state active on storage * chore: update gap of item dropdown model * chore: update select model base on id * chore: update edit model capabilities * chore: add dialog to add model * chore: update sheet for model setting * chore: add sheet setting each model * chore: make dynamic syntax highlight * chore: fix menu setting appearance theme * chore: markdown render support emoji * chore: markdown support latex * chore: change codeblock default theme * chore: update ui codeblock * chore: custom render link taget new window * chore: fix copy button codeblock * chore: update accent and desctructive color * chore: setup user chat message * chore: prepare some page settings * chore: simple list extension and prepare mcp, local api, and hardware * chore: mcp-serve * chore: MCP server UI * chore: update local api server config * chore: adjust chat input * chore: update local api server log * chore: prepare hub page * chore: remove help page * chore: update mock * chore: prepare http proxy setting UI * chore: adjust local api server and title every action * fix: chore FE package (#4962) * fix: update command which referred to non-existent web app * fix: added commented out macos platform for now * fix: remove the platform name as macos * fix: remove unnecessary line for platform name in HeaderPage component * fix: update dev script to specify port 3000 for Vite * feat: model providers and chat completion * enhancement: threads performance * fix: thread content update * chore: clean up threads * fix: performance issue with streaming and state loop * fix: streaming * fix: react markdow * feat: extension manager * chore: add nodePolyfills include path * chore: improve performance avoid unhandle rejection * chore: update pre margin bottom * chore: swith thread should be deafult scroll to bottom * chore: wip scroll to bottom * chore: add model loader * chore: add platform utils * feat: threads functionality * chore: setup toaster * chore: persist threads deletion * fix: create thread with new message * chore: create new thread should change route path * chore: navigate after delet dialog thread * chore: thread favorites and orders * chore: dismiss deleting modal on delete * chore: remove undefined properties * chore: remove deprecated run step * chore: fix delete thread * chore: create empty thread content on started streaming * chore: correct messages store key * chore: stuck at generating state * chore: preapre chat toolbar * chore: introduce in-memory app state * chore: update extensions migration logic * chore: remove redundant extensions migration gate * chore: message toolbar user and assistant * chore: add logo gemini * feat: remote providers with model capabilities * chore: maintain provider settings * chore: move speed token into chat input * chore: temp harcoded model loader * chore: make chat text selectable and truncate model list * chore: update shortcut UI * Feat/implement threads (#4977) * chore: add fuse.js library for enhanced search functionality * feat: implement thread filtering with Fuse.js for improved search capabilities * fix: update the fuseOptions * feat: add search functionality to LeftPanel and refactor thread retrieval logic * refactor: optimize thread filtering and improve search functionality in LeftPanel * fix: more edits * refactor: remove duplicate import of useAppState in StreamingContent component * chore: update navigate after delete all thread * chore: pass prop speedToken from new chat input * chore: persist provider general settings * chore: styling search left panel * chore: cleanup margin * chore: update size icon * chore: improve chat input * chore: imprve list markdown * chore: animate border * feat: local model provider work * chore: persist manually added model * chore: prepare download management ui and show version on general setting * chore: improve pre tag * chore: remove buton install extension and improve light theme download * chore: add missing hardware information handler * chore: cleanup small ui * chore: update default provider settings * fix: missing fs commands * chore: correct provider models * chore: prepare delete model * chore: handle thinking block * chore: fix conditional message toolbar * chore: pophover download select none * enhancement: add prune mode * chore: model settings * chore: bump engine version tauri * chore: update style thinking * chore: add indicator and toogle mcp server * chore: wip hub * chore: update model settings * chore: mvp hub * chore: add function rename title * chore: update function delete message * chore: update rename title * chore: update model settings * chore: persist MCP configs * refactor: clean up utils * chore: add tools to completion request * chore: clean up * chore: ignore assets --------- Co-authored-by: Ivan Leo <ivanleomk@gmail.com> Co-authored-by: Louis <louis@jan.ai>
272 lines
7.8 KiB
TypeScript
272 lines
7.8 KiB
TypeScript
// Available styles from react-syntax-highlighter/prism
|
|
// https://github.com/react-syntax-highlighter/react-syntax-highlighter/blob/master/AVAILABLE_STYLES_PRISM.MD
|
|
|
|
const CODE_BLOCK_STYLES = [
|
|
// Dark themes
|
|
'a11y-dark',
|
|
'atom-dark',
|
|
'darcula',
|
|
'dark',
|
|
'dracula',
|
|
'duotone-dark',
|
|
'gruvbox-dark',
|
|
'material-dark',
|
|
'material-oceanic',
|
|
'night-owl',
|
|
'nord',
|
|
'okaidia',
|
|
'one-dark',
|
|
'shades-of-purple',
|
|
'solarized-dark-atom',
|
|
'synthwave84',
|
|
'twilight',
|
|
'vsc-dark-plus',
|
|
'xonokai',
|
|
|
|
// Light themes
|
|
'coldark-cold',
|
|
'coy',
|
|
'coy-without-shadows',
|
|
'duotone-light',
|
|
'ghcolors',
|
|
'gruvbox-light',
|
|
'material-light',
|
|
'one-light',
|
|
'prism',
|
|
'solarizedlight',
|
|
'vs',
|
|
|
|
// Special themes
|
|
'cb',
|
|
'coldark-dark',
|
|
'duotone-earth',
|
|
'duotone-forest',
|
|
'duotone-sea',
|
|
'duotone-space',
|
|
'funky',
|
|
'holi-theme',
|
|
'hopscotch',
|
|
'lucario',
|
|
'pojoaque',
|
|
'tomorrow',
|
|
'z-touch',
|
|
]
|
|
|
|
import { useCodeblock } from '@/hooks/useCodeblock'
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuTrigger,
|
|
DropdownMenuLabel,
|
|
} from '@/components/ui/dropdown-menu'
|
|
import { cn } from '@/lib/utils'
|
|
import { useState } from 'react'
|
|
import { IconSearch } from '@tabler/icons-react'
|
|
import { Input } from '@/components/ui/input'
|
|
|
|
// Function to format style names to be more readable
|
|
function formatStyleName(style: string): string {
|
|
// Special cases for abbreviations and specific terms
|
|
const specialCases: Record<string, string> = {
|
|
a11y: 'Accessibility',
|
|
cb: 'CB',
|
|
vsc: 'VSCode',
|
|
vs: 'Visual Studio',
|
|
ghcolors: 'GitHub Colors',
|
|
}
|
|
|
|
// Direct mappings for compound names that need special formatting
|
|
const directMappings: Record<string, string> = {
|
|
'solarized-dark-atom': 'Solarized Dark (Atom)',
|
|
'solarizedlight': 'Solarized Light',
|
|
'coy-without-shadows': 'Coy (Without Shadows)',
|
|
'gruvbox-dark': 'Gruvbox Dark',
|
|
'gruvbox-light': 'Gruvbox Light',
|
|
'material-dark': 'Material Dark',
|
|
'material-light': 'Material Light',
|
|
'material-oceanic': 'Material Oceanic',
|
|
'night-owl': 'Night Owl',
|
|
'one-dark': 'One Dark',
|
|
'one-light': 'One Light',
|
|
'shades-of-purple': 'Shades of Purple',
|
|
'coldark-cold': 'Coldark Cold',
|
|
'coldark-dark': 'Coldark Dark',
|
|
'holi-theme': 'Holi Theme',
|
|
'synthwave84': 'Synthwave 84',
|
|
'vsc-dark-plus': 'VSCode Dark+',
|
|
'atom-dark': 'Atom Dark',
|
|
'duotone-dark': 'Duotone Dark',
|
|
'duotone-earth': 'Duotone Earth',
|
|
'duotone-forest': 'Duotone Forest',
|
|
'duotone-light': 'Duotone Light',
|
|
'duotone-sea': 'Duotone Sea',
|
|
'duotone-space': 'Duotone Space',
|
|
}
|
|
|
|
// Check for direct mappings first
|
|
if (directMappings[style]) {
|
|
return directMappings[style]
|
|
}
|
|
|
|
// Process other styles
|
|
return style
|
|
.split('-')
|
|
.map((part) => {
|
|
// Check for special cases
|
|
if (specialCases[part]) {
|
|
return specialCases[part]
|
|
}
|
|
|
|
// Handle duotone prefix (fallback for any not in directMappings)
|
|
if (part.startsWith('duotone')) {
|
|
return 'Duotone ' + part.replace('duotone', '')
|
|
}
|
|
|
|
// Capitalize first letter of each word
|
|
return part.charAt(0).toUpperCase() + part.slice(1)
|
|
})
|
|
.join(' ')
|
|
}
|
|
|
|
export default function CodeBlockStyleSwitcher() {
|
|
const { codeBlockStyle, setCodeBlockStyle } = useCodeblock()
|
|
const [searchQuery, setSearchQuery] = useState('')
|
|
|
|
const changeCodeBlockStyle = (style: string) => {
|
|
setCodeBlockStyle(style)
|
|
}
|
|
|
|
// Extract styles by category
|
|
const darkThemes = CODE_BLOCK_STYLES.slice(1, 20)
|
|
const lightThemes = CODE_BLOCK_STYLES.slice(22, 33)
|
|
const specialThemes = CODE_BLOCK_STYLES.slice(35)
|
|
|
|
// Filter styles based on search query
|
|
const filteredDarkThemes = darkThemes.filter((style) =>
|
|
formatStyleName(style).toLowerCase().includes(searchQuery.toLowerCase())
|
|
)
|
|
|
|
const filteredLightThemes = lightThemes.filter((style) =>
|
|
formatStyleName(style).toLowerCase().includes(searchQuery.toLowerCase())
|
|
)
|
|
|
|
const filteredSpecialThemes = specialThemes.filter((style) =>
|
|
formatStyleName(style).toLowerCase().includes(searchQuery.toLowerCase())
|
|
)
|
|
|
|
return (
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<span
|
|
title="Edit Code Block Style"
|
|
className="flex cursor-pointer items-center gap-1 px-2 py-1 rounded-sm bg-main-view-fg/15 text-sm outline-none text-main-view-fg font-medium"
|
|
>
|
|
{formatStyleName(codeBlockStyle || 'one-light')}
|
|
</span>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent
|
|
align="end"
|
|
className="w-64 max-h-80 overflow-y-auto"
|
|
>
|
|
{/* Search input */}
|
|
<div className="px-2 py-2 sticky -top-1 bg-main-view z-10">
|
|
<div className="relative">
|
|
<IconSearch className="absolute left-2 top-1/2 transform -translate-y-1/2 h-4 w-4" />
|
|
<Input
|
|
type="text"
|
|
placeholder="Search styles..."
|
|
value={searchQuery}
|
|
onClick={(e) => {
|
|
e.stopPropagation()
|
|
}}
|
|
onKeyDown={(e) => {
|
|
e.stopPropagation()
|
|
}}
|
|
onChange={(e) => {
|
|
e.preventDefault()
|
|
e.stopPropagation()
|
|
setSearchQuery(e.target.value)
|
|
}}
|
|
className="w-full pl-8 pr-2"
|
|
autoFocus
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Dark themes */}
|
|
{filteredDarkThemes.length > 0 && (
|
|
<>
|
|
<DropdownMenuLabel className="font-medium text-xs px-2 pt-2 text-main-view-fg/60">
|
|
Dark Themes
|
|
</DropdownMenuLabel>
|
|
{filteredDarkThemes.map((style) => (
|
|
<DropdownMenuItem
|
|
key={style}
|
|
className={cn(
|
|
'cursor-pointer my-0.5',
|
|
codeBlockStyle === style && 'bg-main-view-fg/5'
|
|
)}
|
|
onClick={() => changeCodeBlockStyle(style)}
|
|
>
|
|
{formatStyleName(style)}
|
|
</DropdownMenuItem>
|
|
))}
|
|
</>
|
|
)}
|
|
|
|
{/* Light themes */}
|
|
{filteredLightThemes.length > 0 && (
|
|
<>
|
|
<DropdownMenuLabel className="font-medium text-xs px-2 pt-2 text-main-view-fg/60">
|
|
Light Themes
|
|
</DropdownMenuLabel>
|
|
{filteredLightThemes.map((style) => (
|
|
<DropdownMenuItem
|
|
key={style}
|
|
className={cn(
|
|
'cursor-pointer my-0.5',
|
|
codeBlockStyle === style && 'bg-main-view-fg/5'
|
|
)}
|
|
onClick={() => changeCodeBlockStyle(style)}
|
|
>
|
|
{formatStyleName(style)}
|
|
</DropdownMenuItem>
|
|
))}
|
|
</>
|
|
)}
|
|
|
|
{/* Special themes */}
|
|
{filteredSpecialThemes.length > 0 && (
|
|
<>
|
|
<DropdownMenuLabel className="font-medium text-xs px-2 pt-2 text-main-view-fg/60">
|
|
Special Themes
|
|
</DropdownMenuLabel>
|
|
{filteredSpecialThemes.map((style) => (
|
|
<DropdownMenuItem
|
|
key={style}
|
|
className={cn(
|
|
'cursor-pointer my-0.5',
|
|
codeBlockStyle === style && 'bg-main-view-fg/5'
|
|
)}
|
|
onClick={() => changeCodeBlockStyle(style)}
|
|
>
|
|
{formatStyleName(style)}
|
|
</DropdownMenuItem>
|
|
))}
|
|
</>
|
|
)}
|
|
|
|
{/* No results message */}
|
|
{filteredDarkThemes.length === 0 &&
|
|
filteredLightThemes.length === 0 &&
|
|
filteredSpecialThemes.length === 0 && (
|
|
<div className="px-2 py-4 text-center text-sm text-muted-foreground">
|
|
No styles found
|
|
</div>
|
|
)}
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
)
|
|
}
|