misc: provider icons, tooltip in chat toolbar, conditionally display the "Open Skills Folder" button and update "Documentation" to "Website" in settings (#60)
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
* Supports: file picker, clipboard paste, drag & drop.
|
||||
*/
|
||||
import { useState, useRef, useEffect, useCallback } from 'react';
|
||||
import { Send, Square, ImagePlus, X } from 'lucide-react';
|
||||
import { Send, Square, X } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
|
||||
@@ -200,18 +200,7 @@ export function ChatInput({ onSend, onStop, disabled = false, sending = false }:
|
||||
|
||||
{/* Input Row */}
|
||||
<div className={`flex items-end gap-2 ${dragOver ? 'ring-2 ring-primary rounded-lg' : ''}`}>
|
||||
{/* Image Upload Button */}
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="shrink-0 h-[44px] w-[44px] text-muted-foreground hover:text-foreground"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
disabled={disabled}
|
||||
title="Attach image"
|
||||
>
|
||||
<ImagePlus className="h-5 w-5" />
|
||||
</Button>
|
||||
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
|
||||
@@ -5,8 +5,10 @@
|
||||
*/
|
||||
import { RefreshCw, Brain, ChevronDown, Plus } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import { useChatStore } from '@/stores/chat';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export function ChatToolbar() {
|
||||
const sessions = useChatStore((s) => s.sessions);
|
||||
@@ -17,6 +19,7 @@ export function ChatToolbar() {
|
||||
const loading = useChatStore((s) => s.loading);
|
||||
const showThinking = useChatStore((s) => s.showThinking);
|
||||
const toggleThinking = useChatStore((s) => s.toggleThinking);
|
||||
const { t } = useTranslation('chat');
|
||||
|
||||
const handleSessionChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
switchSession(e.target.value);
|
||||
@@ -51,41 +54,59 @@ export function ChatToolbar() {
|
||||
</div>
|
||||
|
||||
{/* New Session */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8"
|
||||
onClick={newSession}
|
||||
title="New session"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
</Button>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8"
|
||||
onClick={newSession}
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{t('toolbar.newSession')}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
{/* Refresh */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8"
|
||||
onClick={() => refresh()}
|
||||
disabled={loading}
|
||||
title="Refresh chat"
|
||||
>
|
||||
<RefreshCw className={cn('h-4 w-4', loading && 'animate-spin')} />
|
||||
</Button>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8"
|
||||
onClick={() => refresh()}
|
||||
disabled={loading}
|
||||
>
|
||||
<RefreshCw className={cn('h-4 w-4', loading && 'animate-spin')} />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{t('toolbar.refresh')}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
{/* Thinking Toggle */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className={cn(
|
||||
'h-8 w-8',
|
||||
showThinking && 'bg-primary/10 text-primary',
|
||||
)}
|
||||
onClick={toggleThinking}
|
||||
title={showThinking ? 'Hide thinking' : 'Show thinking'}
|
||||
>
|
||||
<Brain className="h-4 w-4" />
|
||||
</Button>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className={cn(
|
||||
'h-8 w-8',
|
||||
showThinking && 'bg-primary/10 text-primary',
|
||||
)}
|
||||
onClick={toggleThinking}
|
||||
>
|
||||
<Brain className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{showThinking ? t('toolbar.hideThinking') : t('toolbar.showThinking')}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -101,7 +101,7 @@ const defaultSkills: DefaultSkill[] = [
|
||||
{ id: 'terminal', name: 'Terminal', description: 'Shell command execution' },
|
||||
];
|
||||
|
||||
import { SETUP_PROVIDERS, type ProviderTypeInfo } from '@/lib/providers';
|
||||
import { SETUP_PROVIDERS, type ProviderTypeInfo, getProviderIconUrl, shouldInvertInDark } from '@/lib/providers';
|
||||
|
||||
// Use the shared provider registry for setup providers
|
||||
const providers = SETUP_PROVIDERS;
|
||||
@@ -1454,7 +1454,7 @@ function CompleteContent({ selectedProvider, installedSkills }: CompleteContentP
|
||||
<div className="flex items-center justify-between p-3 rounded-lg bg-muted/50">
|
||||
<span>{t('complete.provider')}</span>
|
||||
<span className="text-green-400">
|
||||
{providerData ? `${providerData.icon} ${providerData.name}` : '—'}
|
||||
{providerData ? <span className="flex items-center gap-1.5">{getProviderIconUrl(providerData.id) ? <img src={getProviderIconUrl(providerData.id)} alt={providerData.name} className={`h-4 w-4 inline-block ${shouldInvertInDark(providerData.id) ? 'dark:invert' : ''}`} /> : providerData.icon} {providerData.name}</span> : '—'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between p-3 rounded-lg bg-muted/50">
|
||||
|
||||
@@ -615,6 +615,8 @@ export function Skills() {
|
||||
}
|
||||
}, [enableSkill, disableSkill, t]);
|
||||
|
||||
const hasInstalledSkills = skills.some(s => !s.isBundled);
|
||||
|
||||
const handleOpenSkillsFolder = useCallback(async () => {
|
||||
try {
|
||||
const skillsDir = await window.electron.ipcRenderer.invoke('openclaw:getSkillsDir') as string;
|
||||
@@ -623,7 +625,12 @@ export function Skills() {
|
||||
}
|
||||
const result = await window.electron.ipcRenderer.invoke('shell:openPath', skillsDir) as string;
|
||||
if (result) {
|
||||
throw new Error(result);
|
||||
// shell.openPath returns an error string if the path doesn't exist
|
||||
if (result.toLowerCase().includes('no such file') || result.toLowerCase().includes('not found') || result.toLowerCase().includes('failed to open')) {
|
||||
toast.error(t('toast.failedFolderNotFound'));
|
||||
} else {
|
||||
throw new Error(result);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
toast.error(t('toast.failedOpenFolder') + ': ' + String(err));
|
||||
@@ -702,10 +709,12 @@ export function Skills() {
|
||||
<RefreshCw className="h-4 w-4 mr-2" />
|
||||
{t('refresh')}
|
||||
</Button>
|
||||
<Button variant="outline" onClick={handleOpenSkillsFolder}>
|
||||
<FolderOpen className="h-4 w-4 mr-2" />
|
||||
{t('openFolder')}
|
||||
</Button>
|
||||
{hasInstalledSkills && (
|
||||
<Button variant="outline" onClick={handleOpenSkillsFolder}>
|
||||
<FolderOpen className="h-4 w-4 mr-2" />
|
||||
{t('openFolder')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user