Skip to content

Commit d9cb6e3

Browse files
committed
Add model comparison view and a keyboard shortcuts cheat-sheet
- Compare page (new /compare route): pick any combination of installed Ollama models, downloaded llama.cpp models, and cloud models, send one prompt to all of them at once, and watch each response stream in side by side with its own token count and cost estimate. Reachable from the sidebar and the command palette. - Keyboard shortcuts dialog (Ctrl/Cmd+/, or the keyboard icon next to Settings): lists every existing shortcut in one place (command palette, new chat, settings, send/newline, stop generating) - none of these were newly added, just made discoverable. Verified the comparison view end-to-end in a live browser session (mocked window.api with a fake streaming chat.send): selected two models, ran a prompt, confirmed both streamed independently and rendered with correct token/cost info.
1 parent 4633ea6 commit d9cb6e3

7 files changed

Lines changed: 338 additions & 5 deletions

File tree

‎README.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,9 @@ Beyond chat, Modelforge includes an **agentic mode** — the model can read/writ
3636
- **Projects** — group related chats under shared instructions and default model parameters.
3737
- **Per-session and per-project overrides** — pin a specific prompt, model, temperature, seed, top-K/top-P, repeat penalty, context length, GPU offload, or stop sequences to a single chat or an entire project, falling back to sane defaults. Provider-specific parameters (e.g. seed isn't supported by Claude, top-K isn't supported by ChatGPT) are automatically disabled when they don't apply to the selected model.
3838
- **Prompt library** — save and reuse system prompts across chats. Prompts can include `{{variables}}` (e.g. `{{topic}}`) that you fill in each time you apply one, and edits keep version history so a bad change can be restored.
39-
- **Command palette** (`Ctrl/Cmd+K`) — jump between chats, projects, and settings without touching the mouse.
39+
- **Command palette** (`Ctrl/Cmd+K`) — jump between chats, projects, settings, and Compare without touching the mouse.
40+
- **Keyboard shortcuts cheat-sheet** (`Ctrl/Cmd+/`, or the keyboard icon next to Settings) — everything you can do without the mouse, in one place.
41+
- **Compare models** — send one prompt to several models at once (local or cloud) and see every response stream in side by side, with per-model token counts and cost estimates.
4042
- **Full-text search** — the sidebar search box matches message content across every chat, not just titles.
4143
- **Tags** — label chats with freeform tags and filter the sidebar by them; lighter-weight than Projects for ad-hoc organization.
4244
- **Pin messages** — bookmark any message in a chat and jump straight to it from a "Pinned" panel in the toolbar.

‎frontend/src/App.tsx‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { createHashRouter, RouterProvider } from "react-router-dom";
22
import Layout from "./components/layout";
33
import Chat from "./pages/Chat";
44
import Settings from "./pages/Settings";
5+
import Compare from "./pages/Compare";
56
import { ThemeProvider } from "@/components/theme-provider";
67
import { SessionsProvider } from "@/lib/sessions-context";
78
import { I18nProvider } from "@/lib/i18n";
@@ -14,6 +15,7 @@ const router = createHashRouter([
1415
{ index: true, element: <Chat /> },
1516
{ path: "chat/:sessionId", element: <Chat /> },
1617
{ path: "settings", element: <Settings /> },
18+
{ path: "compare", element: <Compare /> },
1719
],
1820
},
1921
]);

‎frontend/src/components/command-palette.tsx‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { FolderOpen, MessageSquare, Plus, Settings as SettingsIcon } from "lucide-react";
1+
import { FolderOpen, MessageSquare, Plus, Settings as SettingsIcon, Scale } from "lucide-react";
22
import {
33
CommandDialog,
44
CommandEmpty,
@@ -17,6 +17,7 @@ export function CommandPalette({
1717
onNewChat,
1818
onOpenSession,
1919
onNavigateSettings,
20+
onNavigateCompare,
2021
}: {
2122
open: boolean;
2223
onOpenChange: (open: boolean) => void;
@@ -25,6 +26,7 @@ export function CommandPalette({
2526
onNewChat: (projectId?: string) => void;
2627
onOpenSession: (id: string) => void;
2728
onNavigateSettings: () => void;
29+
onNavigateCompare: () => void;
2830
}) {
2931
function run(action: () => void) {
3032
action();
@@ -43,6 +45,9 @@ export function CommandPalette({
4345
<CommandItem onSelect={() => run(onNavigateSettings)}>
4446
<SettingsIcon /> Settings
4547
</CommandItem>
48+
<CommandItem onSelect={() => run(onNavigateCompare)}>
49+
<Scale /> Compare models
50+
</CommandItem>
4651
</CommandGroup>
4752

4853
{projects.length > 0 && (
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog";
2+
import { useI18n } from "@/lib/i18n";
3+
4+
const isMac = typeof navigator !== "undefined" && /Mac|iPod|iPhone|iPad/.test(navigator.platform);
5+
const mod = isMac ? "⌘" : "Ctrl";
6+
7+
function Shortcut({ label, keys }: { label: string; keys: string[] }) {
8+
return (
9+
<div className="flex items-center justify-between gap-4 py-1.5">
10+
<span className="text-sm text-muted-foreground">{label}</span>
11+
<div className="flex gap-1">
12+
{keys.map((k, i) => (
13+
<kbd
14+
key={i}
15+
className="rounded border border-border bg-muted px-1.5 py-0.5 font-mono text-xs text-foreground"
16+
>
17+
{k}
18+
</kbd>
19+
))}
20+
</div>
21+
</div>
22+
);
23+
}
24+
25+
export function KeyboardShortcutsDialog({
26+
open,
27+
onOpenChange,
28+
}: {
29+
open: boolean;
30+
onOpenChange: (open: boolean) => void;
31+
}) {
32+
const { t } = useI18n();
33+
34+
return (
35+
<Dialog open={open} onOpenChange={onOpenChange}>
36+
<DialogContent>
37+
<DialogHeader>
38+
<DialogTitle>{t.keyboardShortcuts}</DialogTitle>
39+
<DialogDescription>{t.keyboardShortcutsHelp}</DialogDescription>
40+
</DialogHeader>
41+
<div className="flex flex-col divide-y divide-border">
42+
<Shortcut label={t.shortcutCommandPalette} keys={[mod, "K"]} />
43+
<Shortcut label={t.shortcutNewChat} keys={[mod, "N"]} />
44+
<Shortcut label={t.shortcutSettings} keys={[mod, ","]} />
45+
<Shortcut label={t.shortcutShowShortcuts} keys={[mod, "/"]} />
46+
<Shortcut label={t.shortcutSend} keys={["Enter"]} />
47+
<Shortcut label={t.shortcutNewline} keys={["Shift", "Enter"]} />
48+
<Shortcut label={t.shortcutStopGenerating} keys={["Esc"]} />
49+
</div>
50+
</DialogContent>
51+
</Dialog>
52+
);
53+
}

‎frontend/src/components/layout.tsx‎

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import {
1515
Tag,
1616
Trash2,
1717
X,
18+
Keyboard,
19+
Scale,
1820
} from "lucide-react";
1921
import { Button } from "@/components/ui/button";
2022
import { Input } from "@/components/ui/input";
@@ -23,6 +25,7 @@ import { ScrollArea } from "@/components/ui/scroll-area";
2325
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
2426
import { ThemeToggle } from "@/components/theme-toggle";
2527
import { CommandPalette } from "@/components/command-palette";
28+
import { KeyboardShortcutsDialog } from "@/components/keyboard-shortcuts-dialog";
2629
import { useSessions } from "@/lib/sessions-context";
2730
import { useI18n } from "@/lib/i18n";
2831
import { cn } from "@/lib/utils";
@@ -509,6 +512,7 @@ export default function Layout() {
509512
const [creatingProject, setCreatingProject] = useState(false);
510513
const [newProjectName, setNewProjectName] = useState("");
511514
const [paletteOpen, setPaletteOpen] = useState(false);
515+
const [shortcutsOpen, setShortcutsOpen] = useState(false);
512516

513517
const query = search.trim().toLowerCase();
514518
const matchesSearch = (s: ChatSession) =>
@@ -570,12 +574,16 @@ export default function Layout() {
570574
// eslint-disable-next-line react-hooks/exhaustive-deps
571575
}, [hasApi]);
572576

573-
// Ctrl/Cmd+K opens the command palette from anywhere in the app.
577+
// Ctrl/Cmd+K opens the command palette, Ctrl/Cmd+/ shows the shortcuts
578+
// cheat-sheet, both from anywhere in the app.
574579
useEffect(() => {
575580
function onKeyDown(e: KeyboardEvent) {
576581
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "k") {
577582
e.preventDefault();
578583
setPaletteOpen((o) => !o);
584+
} else if ((e.ctrlKey || e.metaKey) && e.key === "/") {
585+
e.preventDefault();
586+
setShortcutsOpen((o) => !o);
579587
}
580588
}
581589
window.addEventListener("keydown", onKeyDown);
@@ -633,6 +641,16 @@ export default function Layout() {
633641
</Button>
634642
</div>
635643
)}
644+
<Button
645+
onClick={() => navigate("/compare")}
646+
size="sm"
647+
variant="ghost"
648+
className="w-full justify-start gap-2 text-muted-foreground"
649+
disabled={!hasApi}
650+
>
651+
<Scale className="size-4" />
652+
{t.compareModels}
653+
</Button>
636654
</div>
637655

638656
<div className="relative px-3 pb-2">
@@ -698,16 +716,26 @@ export default function Layout() {
698716
</div>
699717
</ScrollArea>
700718

701-
<div className="border-t border-border p-2">
719+
<div className="flex border-t border-border p-2">
702720
<Button
703721
variant="ghost"
704722
size="sm"
705-
className="w-full justify-start gap-2"
723+
className="flex-1 justify-start gap-2"
706724
onClick={() => navigate("/settings")}
707725
>
708726
<SettingsIcon className="size-4" />
709727
{t.settings}
710728
</Button>
729+
<Button
730+
variant="ghost"
731+
size="icon"
732+
className="shrink-0"
733+
onClick={() => setShortcutsOpen(true)}
734+
aria-label={t.keyboardShortcuts}
735+
title={t.keyboardShortcuts}
736+
>
737+
<Keyboard className="size-4" />
738+
</Button>
711739
</div>
712740
</aside>
713741

@@ -723,7 +751,9 @@ export default function Layout() {
723751
onNewChat={(projectId) => handleNewChat(projectId)}
724752
onOpenSession={(id) => navigate(`/chat/${id}`)}
725753
onNavigateSettings={() => navigate("/settings")}
754+
onNavigateCompare={() => navigate("/compare")}
726755
/>
756+
<KeyboardShortcutsDialog open={shortcutsOpen} onOpenChange={setShortcutsOpen} />
727757
</div>
728758
);
729759
}

‎frontend/src/lib/translations.ts‎

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,20 @@ export interface Dictionary {
168168
settingsTabChat: string;
169169
settingsTabVoice: string;
170170
settingsTabData: string;
171+
keyboardShortcuts: string;
172+
keyboardShortcutsHelp: string;
173+
shortcutCommandPalette: string;
174+
shortcutNewChat: string;
175+
shortcutSettings: string;
176+
shortcutShowShortcuts: string;
177+
shortcutSend: string;
178+
shortcutNewline: string;
179+
shortcutStopGenerating: string;
180+
compareModels: string;
181+
compareSamePrompt: string;
182+
compareRun: string;
183+
compareRunToSee: string;
184+
noModelsAvailable: string;
171185
resetToDefault: string;
172186
usingCustomPrompt: string;
173187
dataManagement: string;
@@ -374,6 +388,20 @@ export const en: Dictionary = {
374388
settingsTabChat: "Chat & Prompts",
375389
settingsTabVoice: "Voice",
376390
settingsTabData: "Data",
391+
keyboardShortcuts: "Keyboard shortcuts",
392+
keyboardShortcutsHelp: "Everything you can do without touching the mouse.",
393+
shortcutCommandPalette: "Command palette",
394+
shortcutNewChat: "New chat",
395+
shortcutSettings: "Open settings",
396+
shortcutShowShortcuts: "Show this dialog",
397+
shortcutSend: "Send message",
398+
shortcutNewline: "New line",
399+
shortcutStopGenerating: "Stop generating",
400+
compareModels: "Compare models",
401+
compareSamePrompt: "Type a prompt to send to every selected model at once...",
402+
compareRun: "Run",
403+
compareRunToSee: "Run the comparison to see this model's response.",
404+
noModelsAvailable: "No models available yet — install one in Settings first.",
377405
resetToDefault: "Reset to default",
378406
usingCustomPrompt: "Custom prompt for this chat",
379407
dataManagement: "Data management",
@@ -588,6 +616,20 @@ export const tr: Dictionary = {
588616
settingsTabChat: "Sohbet ve Promptlar",
589617
settingsTabVoice: "Ses",
590618
settingsTabData: "Veri",
619+
keyboardShortcuts: "Klavye kısayolları",
620+
keyboardShortcutsHelp: "Fareye dokunmadan yapabileceğiniz her şey.",
621+
shortcutCommandPalette: "Komut paleti",
622+
shortcutNewChat: "Yeni sohbet",
623+
shortcutSettings: "Ayarları aç",
624+
shortcutShowShortcuts: "Bu pencereyi göster",
625+
shortcutSend: "Mesaj gönder",
626+
shortcutNewline: "Yeni satır",
627+
shortcutStopGenerating: "Üretimi durdur",
628+
compareModels: "Modelleri karşılaştır",
629+
compareSamePrompt: "Seçili tüm modellere aynı anda gönderilecek bir prompt yazın...",
630+
compareRun: "Çalıştır",
631+
compareRunToSee: "Bu modelin yanıtını görmek için karşılaştırmayı çalıştırın.",
632+
noModelsAvailable: "Henüz kullanılabilir model yok — önce Ayarlar'dan bir tane kurun.",
591633
resetToDefault: "Varsayılana dön",
592634
usingCustomPrompt: "Bu sohbet için özel istem",
593635
dataManagement: "Veri yönetimi",

0 commit comments

Comments
 (0)