llm-interceptor 2.2.0__tar.gz → 2.2.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/CHANGELOG.md +10 -0
- {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/PKG-INFO +1 -1
- {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/pyproject.toml +1 -1
- {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/src/cci/__init__.py +1 -1
- {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/src/cci/cli.py +16 -4
- {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/ui/README.md +5 -0
- llm_interceptor-2.2.1/ui/src/components/layout/SessionsSidebar.tsx +311 -0
- llm_interceptor-2.2.0/ui/src/components/layout/SessionsSidebar.tsx +0 -271
- {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/.github/workflows/ci.yml +0 -0
- {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/.github/workflows/publish.yml +0 -0
- {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/.gitignore +0 -0
- {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/.pre-commit-config.yaml +0 -0
- {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/README.md +0 -0
- {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/build_ui.py +0 -0
- {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/cci-ui-screenshot.png +0 -0
- {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/cci.example.toml +0 -0
- {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/lli.example.toml +0 -0
- {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/src/cci/app.py +0 -0
- {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/src/cci/config.py +0 -0
- {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/src/cci/filters.py +0 -0
- {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/src/cci/logger.py +0 -0
- {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/src/cci/merger.py +0 -0
- {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/src/cci/models.py +0 -0
- {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/src/cci/proxy.py +0 -0
- {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/src/cci/server.py +0 -0
- {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/src/cci/splitter.py +0 -0
- {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/src/cci/storage.py +0 -0
- {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/src/cci/watch.py +0 -0
- {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/src/llm_interceptor/__init__.py +0 -0
- {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/src/llm_interceptor/cli.py +0 -0
- {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/tests/__init__.py +0 -0
- {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/tests/test_basic.py +0 -0
- {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/tests/test_merger.py +0 -0
- {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/tests/test_watch_cancel.py +0 -0
- {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/ui/.gitignore +0 -0
- {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/ui/eslint.config.js +0 -0
- {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/ui/index.html +0 -0
- {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/ui/package-lock.json +0 -0
- {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/ui/package.json +0 -0
- {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/ui/postcss.config.js +0 -0
- {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/ui/public/cci-icon.png +0 -0
- {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/ui/public/vite.svg +0 -0
- {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/ui/src/App.css +0 -0
- {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/ui/src/App.tsx +0 -0
- {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/ui/src/assets/react.svg +0 -0
- {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/ui/src/components/chat/ChatBubble.tsx +0 -0
- {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/ui/src/components/chat/MessageContent.tsx +0 -0
- {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/ui/src/components/chat/ToolCallBlock.tsx +0 -0
- {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/ui/src/components/common/CopyButton.tsx +0 -0
- {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/ui/src/components/common/JSONViewer.tsx +0 -0
- {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/ui/src/components/common/TokenBadge.tsx +0 -0
- {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/ui/src/components/common/Tooltip.tsx +0 -0
- {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/ui/src/components/layout/EmptyState.tsx +0 -0
- {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/ui/src/components/layout/ExchangeDetailsPane.tsx +0 -0
- {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/ui/src/components/layout/RequestsPane.tsx +0 -0
- {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/ui/src/components/tools/ToolsList.tsx +0 -0
- {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/ui/src/hooks/useAnnotations.ts +0 -0
- {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/ui/src/hooks/useResizablePanels.ts +0 -0
- {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/ui/src/hooks/useSessions.ts +0 -0
- {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/ui/src/hooks/useTheme.ts +0 -0
- {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/ui/src/index.css +0 -0
- {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/ui/src/main.tsx +0 -0
- {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/ui/src/types.ts +0 -0
- {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/ui/src/utils/ui.ts +0 -0
- {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/ui/src/utils.ts +0 -0
- {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/ui/tailwind.config.js +0 -0
- {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/ui/tsconfig.app.json +0 -0
- {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/ui/tsconfig.json +0 -0
- {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/ui/tsconfig.node.json +0 -0
- {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/ui/vite.config.ts +0 -0
|
@@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [2.2.1] - 2026-01-06
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **Configurable UI host/port** - Allow configuring the UI host and port
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- **React hooks usage** - Avoid hooks in sessions map to prevent runtime errors
|
|
17
|
+
|
|
8
18
|
## [2.2.0] - 2026-01-05
|
|
9
19
|
|
|
10
20
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: llm-interceptor
|
|
3
|
-
Version: 2.2.
|
|
3
|
+
Version: 2.2.1
|
|
4
4
|
Summary: Intercept and analyze LLM traffic from AI coding tools
|
|
5
5
|
Project-URL: Homepage, https://github.com/chouzz/llm-interceptor
|
|
6
6
|
Project-URL: Documentation, https://github.com/chouzz/llm-interceptor#readme
|
|
@@ -400,6 +400,19 @@ def stats(file: str) -> None:
|
|
|
400
400
|
default=True,
|
|
401
401
|
help="Launch the web UI (default: True)",
|
|
402
402
|
)
|
|
403
|
+
@click.option(
|
|
404
|
+
"--ui-host",
|
|
405
|
+
default="127.0.0.1",
|
|
406
|
+
show_default=True,
|
|
407
|
+
help="Host interface for the web UI (use 0.0.0.0 to expose on the network)",
|
|
408
|
+
)
|
|
409
|
+
@click.option(
|
|
410
|
+
"--ui-port",
|
|
411
|
+
default=8000,
|
|
412
|
+
show_default=True,
|
|
413
|
+
type=int,
|
|
414
|
+
help="Port for the web UI",
|
|
415
|
+
)
|
|
403
416
|
@click.pass_context
|
|
404
417
|
def watch(
|
|
405
418
|
ctx: click.Context,
|
|
@@ -408,6 +421,8 @@ def watch(
|
|
|
408
421
|
include: tuple[str, ...],
|
|
409
422
|
debug: bool,
|
|
410
423
|
ui: bool,
|
|
424
|
+
ui_host: str,
|
|
425
|
+
ui_port: int,
|
|
411
426
|
) -> None:
|
|
412
427
|
"""
|
|
413
428
|
Start watch mode for continuous session capture.
|
|
@@ -475,9 +490,6 @@ def watch(
|
|
|
475
490
|
if ui:
|
|
476
491
|
from cci.server import run_server
|
|
477
492
|
|
|
478
|
-
ui_host = "127.0.0.1"
|
|
479
|
-
ui_port = 8000 # TODO: Make configurable
|
|
480
|
-
|
|
481
493
|
if _is_port_in_use(ui_host, ui_port):
|
|
482
494
|
ui_url = f"http://{ui_host}:{ui_port}"
|
|
483
495
|
console.print(
|
|
@@ -493,7 +505,7 @@ def watch(
|
|
|
493
505
|
server_thread = threading.Thread(
|
|
494
506
|
target=run_server,
|
|
495
507
|
args=(watch_manager,),
|
|
496
|
-
kwargs={"port": ui_port},
|
|
508
|
+
kwargs={"host": ui_host, "port": ui_port},
|
|
497
509
|
daemon=True,
|
|
498
510
|
)
|
|
499
511
|
server_thread.start()
|
|
@@ -23,6 +23,11 @@ This UI is bundled with the `llm-interceptor` Python package.
|
|
|
23
23
|
lli watch --include "*example.com*"
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
+
To expose the UI on your network:
|
|
27
|
+
```bash
|
|
28
|
+
lli watch --ui-host 0.0.0.0 --ui-port 8000
|
|
29
|
+
```
|
|
30
|
+
|
|
26
31
|
2. **Open the Dashboard**:
|
|
27
32
|
The CLI will print a URL (usually `http://localhost:8000`). Click it to open the dashboard.
|
|
28
33
|
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import React, { useState, useCallback, useMemo } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Activity,
|
|
4
|
+
Check,
|
|
5
|
+
ChevronLeft,
|
|
6
|
+
ChevronRight,
|
|
7
|
+
FolderOpen,
|
|
8
|
+
MessageCircle,
|
|
9
|
+
Moon,
|
|
10
|
+
Pencil,
|
|
11
|
+
Sun,
|
|
12
|
+
} from 'lucide-react';
|
|
13
|
+
import type { AnnotationData, SessionSummary } from '../../types';
|
|
14
|
+
import { formatTimestamp } from '../../utils';
|
|
15
|
+
import { Tooltip } from '../common/Tooltip';
|
|
16
|
+
|
|
17
|
+
export const SessionsSidebar: React.FC<{
|
|
18
|
+
width: number;
|
|
19
|
+
isCollapsed: boolean;
|
|
20
|
+
setIsCollapsed: (v: boolean) => void;
|
|
21
|
+
onStartResize: (e: React.MouseEvent) => void;
|
|
22
|
+
|
|
23
|
+
sessionList: SessionSummary[];
|
|
24
|
+
selectedSessionId: string | null;
|
|
25
|
+
onSelectSession: (sessionId: string) => void;
|
|
26
|
+
|
|
27
|
+
isDarkMode: boolean;
|
|
28
|
+
onToggleTheme: () => void;
|
|
29
|
+
|
|
30
|
+
annotations: Record<string, AnnotationData>;
|
|
31
|
+
onUpdateSessionNote: (sessionId: string, note: string) => void;
|
|
32
|
+
}> = ({
|
|
33
|
+
width,
|
|
34
|
+
isCollapsed,
|
|
35
|
+
setIsCollapsed,
|
|
36
|
+
onStartResize,
|
|
37
|
+
sessionList,
|
|
38
|
+
selectedSessionId,
|
|
39
|
+
onSelectSession,
|
|
40
|
+
isDarkMode,
|
|
41
|
+
onToggleTheme,
|
|
42
|
+
annotations,
|
|
43
|
+
onUpdateSessionNote,
|
|
44
|
+
}) => {
|
|
45
|
+
const [editingSessionNote, setEditingSessionNote] = useState<string | null>(null);
|
|
46
|
+
|
|
47
|
+
// Memoize callback functions
|
|
48
|
+
const handleToggleCollapse = useCallback(() => {
|
|
49
|
+
setIsCollapsed(!isCollapsed);
|
|
50
|
+
}, [isCollapsed, setIsCollapsed]);
|
|
51
|
+
|
|
52
|
+
// Memoize rendered sessions
|
|
53
|
+
const renderedSessions = useMemo(() => {
|
|
54
|
+
return sessionList.map((session) => {
|
|
55
|
+
const sessionNote = annotations[session.id]?.session_note || '';
|
|
56
|
+
const hasNote = sessionNote.length > 0;
|
|
57
|
+
const isEditing = editingSessionNote === session.id;
|
|
58
|
+
const isSelected = selectedSessionId === session.id;
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<SessionItem
|
|
62
|
+
key={session.id}
|
|
63
|
+
session={session}
|
|
64
|
+
isCollapsed={isCollapsed}
|
|
65
|
+
isSelected={isSelected}
|
|
66
|
+
isEditing={isEditing}
|
|
67
|
+
sessionNote={sessionNote}
|
|
68
|
+
hasNote={hasNote}
|
|
69
|
+
onSelectSession={onSelectSession}
|
|
70
|
+
onSetIsCollapsed={setIsCollapsed}
|
|
71
|
+
onSetEditingSessionNote={setEditingSessionNote}
|
|
72
|
+
onUpdateSessionNote={onUpdateSessionNote}
|
|
73
|
+
/>
|
|
74
|
+
);
|
|
75
|
+
});
|
|
76
|
+
}, [
|
|
77
|
+
sessionList,
|
|
78
|
+
annotations,
|
|
79
|
+
editingSessionNote,
|
|
80
|
+
selectedSessionId,
|
|
81
|
+
isCollapsed,
|
|
82
|
+
onSelectSession,
|
|
83
|
+
setIsCollapsed,
|
|
84
|
+
onUpdateSessionNote,
|
|
85
|
+
]);
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<div
|
|
89
|
+
style={{ width: isCollapsed ? '48px' : width }}
|
|
90
|
+
className="flex-shrink-0 border-r border-gray-200 dark:border-slate-800 bg-white dark:bg-[#0b1120] flex flex-col relative transition-all duration-300 ease-in-out"
|
|
91
|
+
>
|
|
92
|
+
{/* Sessions Header */}
|
|
93
|
+
<div
|
|
94
|
+
className={`p-4 border-b border-gray-200 dark:border-slate-800 flex items-center ${
|
|
95
|
+
isCollapsed ? 'justify-center flex-col gap-4' : 'justify-between'
|
|
96
|
+
}`}
|
|
97
|
+
>
|
|
98
|
+
{!isCollapsed && (
|
|
99
|
+
<div className="flex items-center gap-2">
|
|
100
|
+
<Activity size={18} className="text-blue-600 dark:text-blue-500" />
|
|
101
|
+
<h2 className="font-bold text-sm tracking-wide text-slate-700 dark:text-slate-200">
|
|
102
|
+
SESSIONS
|
|
103
|
+
</h2>
|
|
104
|
+
</div>
|
|
105
|
+
)}
|
|
106
|
+
|
|
107
|
+
<div className={`flex ${isCollapsed ? 'flex-col gap-3' : 'gap-1'}`}>
|
|
108
|
+
<button
|
|
109
|
+
onClick={handleToggleCollapse}
|
|
110
|
+
className="p-1.5 hover:bg-gray-100 dark:hover:bg-slate-800 rounded text-slate-500 dark:text-slate-400 transition-colors"
|
|
111
|
+
title={isCollapsed ? 'Expand' : 'Collapse'}
|
|
112
|
+
type="button"
|
|
113
|
+
>
|
|
114
|
+
{isCollapsed ? <ChevronRight size={14} /> : <ChevronLeft size={14} />}
|
|
115
|
+
</button>
|
|
116
|
+
{!isCollapsed && (
|
|
117
|
+
<button
|
|
118
|
+
onClick={onToggleTheme}
|
|
119
|
+
className="p-1.5 hover:bg-gray-100 dark:hover:bg-slate-800 rounded text-slate-500 dark:text-slate-400 transition-colors"
|
|
120
|
+
title="Toggle Theme"
|
|
121
|
+
type="button"
|
|
122
|
+
>
|
|
123
|
+
{isDarkMode ? <Sun size={14} /> : <Moon size={14} />}
|
|
124
|
+
</button>
|
|
125
|
+
)}
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
|
|
129
|
+
{/* Sessions List */}
|
|
130
|
+
<div className="overflow-y-auto flex-1 p-2 space-y-1 custom-scrollbar">
|
|
131
|
+
{renderedSessions}
|
|
132
|
+
</div>
|
|
133
|
+
|
|
134
|
+
{/* Resizer Handle */}
|
|
135
|
+
{!isCollapsed && (
|
|
136
|
+
<div
|
|
137
|
+
className="absolute top-0 right-0 w-1 h-full cursor-col-resize hover:bg-blue-500/50 transition-colors z-10 flex items-center justify-center group"
|
|
138
|
+
onMouseDown={onStartResize}
|
|
139
|
+
>
|
|
140
|
+
<div className="w-[1px] h-full bg-gray-200 dark:bg-slate-800 group-hover:bg-blue-500"></div>
|
|
141
|
+
</div>
|
|
142
|
+
)}
|
|
143
|
+
</div>
|
|
144
|
+
);
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const SessionItem = React.memo<{
|
|
148
|
+
session: SessionSummary;
|
|
149
|
+
isCollapsed: boolean;
|
|
150
|
+
isSelected: boolean;
|
|
151
|
+
isEditing: boolean;
|
|
152
|
+
sessionNote: string;
|
|
153
|
+
hasNote: boolean;
|
|
154
|
+
onSelectSession: (sessionId: string) => void;
|
|
155
|
+
onSetIsCollapsed: (collapsed: boolean) => void;
|
|
156
|
+
onSetEditingSessionNote: (sessionId: string | null) => void;
|
|
157
|
+
onUpdateSessionNote: (sessionId: string, note: string) => void;
|
|
158
|
+
}>(({
|
|
159
|
+
session,
|
|
160
|
+
isCollapsed,
|
|
161
|
+
isSelected,
|
|
162
|
+
isEditing,
|
|
163
|
+
sessionNote,
|
|
164
|
+
hasNote,
|
|
165
|
+
onSelectSession,
|
|
166
|
+
onSetIsCollapsed,
|
|
167
|
+
onSetEditingSessionNote,
|
|
168
|
+
onUpdateSessionNote,
|
|
169
|
+
}) => {
|
|
170
|
+
const handleSelectSession = useCallback(() => {
|
|
171
|
+
onSelectSession(session.id);
|
|
172
|
+
if (isCollapsed) onSetIsCollapsed(false);
|
|
173
|
+
}, [session.id, onSelectSession, isCollapsed, onSetIsCollapsed]);
|
|
174
|
+
|
|
175
|
+
const handleToggleEdit = useCallback(() => {
|
|
176
|
+
onSetEditingSessionNote(isEditing ? null : session.id);
|
|
177
|
+
}, [isEditing, session.id, onSetEditingSessionNote]);
|
|
178
|
+
|
|
179
|
+
const handleUpdateNote = useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
|
180
|
+
onUpdateSessionNote(session.id, e.target.value);
|
|
181
|
+
}, [session.id, onUpdateSessionNote]);
|
|
182
|
+
|
|
183
|
+
const handleKeyDown = useCallback((e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
|
184
|
+
if (e.key === 'Escape') {
|
|
185
|
+
onSetEditingSessionNote(null);
|
|
186
|
+
} else if (e.key === 'Enter' && !e.shiftKey) {
|
|
187
|
+
e.preventDefault();
|
|
188
|
+
onSetEditingSessionNote(null);
|
|
189
|
+
}
|
|
190
|
+
}, [onSetEditingSessionNote]);
|
|
191
|
+
|
|
192
|
+
const handleSaveNote = useCallback(() => {
|
|
193
|
+
onSetEditingSessionNote(null);
|
|
194
|
+
}, [onSetEditingSessionNote]);
|
|
195
|
+
|
|
196
|
+
const handleEditNote = useCallback(() => {
|
|
197
|
+
onSetEditingSessionNote(session.id);
|
|
198
|
+
}, [session.id, onSetEditingSessionNote]);
|
|
199
|
+
|
|
200
|
+
return (
|
|
201
|
+
<div>
|
|
202
|
+
{/* Collapsed state */}
|
|
203
|
+
{isCollapsed ? (
|
|
204
|
+
<div
|
|
205
|
+
onClick={handleSelectSession}
|
|
206
|
+
className={`h-12 flex items-center justify-center cursor-pointer border-b border-gray-100 dark:border-slate-800/50 relative rounded-lg mb-1 ${
|
|
207
|
+
isSelected ? 'bg-blue-50 dark:bg-slate-800' : ''
|
|
208
|
+
}`}
|
|
209
|
+
>
|
|
210
|
+
<FolderOpen
|
|
211
|
+
size={20}
|
|
212
|
+
className={
|
|
213
|
+
isSelected
|
|
214
|
+
? 'text-blue-600 dark:text-blue-400'
|
|
215
|
+
: 'text-slate-400 dark:text-slate-600'
|
|
216
|
+
}
|
|
217
|
+
/>
|
|
218
|
+
{hasNote && <div className="absolute top-1 right-1 w-2 h-2 bg-amber-500 rounded-full"></div>}
|
|
219
|
+
</div>
|
|
220
|
+
) : (
|
|
221
|
+
<div>
|
|
222
|
+
<div
|
|
223
|
+
onClick={handleSelectSession}
|
|
224
|
+
className={`px-3 py-3 border-b border-gray-100 dark:border-slate-800/50 cursor-pointer transition-colors group relative rounded-lg mb-1 ${
|
|
225
|
+
isSelected ? 'bg-blue-50 dark:bg-slate-800/80 shadow-md z-10' : 'hover:bg-gray-50 dark:hover:bg-slate-800/30'
|
|
226
|
+
}`}
|
|
227
|
+
title={session.id}
|
|
228
|
+
>
|
|
229
|
+
{/* Action Buttons (appear on hover) */}
|
|
230
|
+
<div className="absolute right-2 top-2 flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity z-20">
|
|
231
|
+
<button
|
|
232
|
+
onClick={(e) => { e.stopPropagation(); handleToggleEdit(); }}
|
|
233
|
+
className="p-1.5 bg-white dark:bg-slate-700 rounded shadow-sm hover:scale-110"
|
|
234
|
+
title="Edit note"
|
|
235
|
+
type="button"
|
|
236
|
+
>
|
|
237
|
+
<Pencil size={12} className="text-slate-500 dark:text-slate-300" />
|
|
238
|
+
</button>
|
|
239
|
+
</div>
|
|
240
|
+
|
|
241
|
+
{/* Selection indicator */}
|
|
242
|
+
{isSelected && (
|
|
243
|
+
<div className="absolute left-0 top-3 bottom-3 w-1 bg-blue-500 rounded-r-full"></div>
|
|
244
|
+
)}
|
|
245
|
+
|
|
246
|
+
<div className="flex items-center justify-between mb-1">
|
|
247
|
+
<div className="flex items-center gap-2">
|
|
248
|
+
<FolderOpen
|
|
249
|
+
size={16}
|
|
250
|
+
className={
|
|
251
|
+
isSelected
|
|
252
|
+
? 'text-blue-600 dark:text-blue-400'
|
|
253
|
+
: 'text-slate-400 dark:text-slate-600'
|
|
254
|
+
}
|
|
255
|
+
/>
|
|
256
|
+
<span className="font-semibold truncate max-w-[120px] text-slate-700 dark:text-slate-200" title={session.id}>
|
|
257
|
+
{session.id.replace('session_', '')}
|
|
258
|
+
</span>
|
|
259
|
+
</div>
|
|
260
|
+
<div className="flex items-center gap-1 text-xs text-slate-400 dark:text-slate-500">
|
|
261
|
+
<span>{session.request_count}</span>
|
|
262
|
+
{hasNote && !isEditing && <MessageCircle size={10} className="text-amber-500" />}
|
|
263
|
+
</div>
|
|
264
|
+
</div>
|
|
265
|
+
<div className="text-xs text-slate-400 dark:text-slate-500">
|
|
266
|
+
{formatTimestamp(session.timestamp)}
|
|
267
|
+
</div>
|
|
268
|
+
|
|
269
|
+
{/* Note Section - inside the card */}
|
|
270
|
+
{!isEditing && hasNote && (
|
|
271
|
+
<Tooltip text={sessionNote}>
|
|
272
|
+
<div
|
|
273
|
+
className="mt-2 px-2 py-1 text-[10px] text-amber-700 dark:text-amber-300 bg-amber-50 dark:bg-amber-950/20 rounded border-l-2 border-amber-400 dark:border-amber-600 cursor-pointer hover:bg-amber-100 dark:hover:bg-amber-950/30 transition-colors truncate"
|
|
274
|
+
onClick={(e) => { e.stopPropagation(); handleEditNote(); }}
|
|
275
|
+
title="Click to edit"
|
|
276
|
+
>
|
|
277
|
+
{sessionNote}
|
|
278
|
+
</div>
|
|
279
|
+
</Tooltip>
|
|
280
|
+
)}
|
|
281
|
+
|
|
282
|
+
{/* Note Editor - inside the card */}
|
|
283
|
+
{isEditing && (
|
|
284
|
+
<div className="mt-2 relative">
|
|
285
|
+
<textarea
|
|
286
|
+
autoFocus
|
|
287
|
+
value={sessionNote}
|
|
288
|
+
onChange={handleUpdateNote}
|
|
289
|
+
onKeyDown={handleKeyDown}
|
|
290
|
+
onBlur={handleSaveNote}
|
|
291
|
+
placeholder="Add a note..."
|
|
292
|
+
className="w-full text-xs p-1.5 pr-6 border border-amber-300 dark:border-amber-700 rounded bg-amber-50 dark:bg-amber-950/30 text-slate-700 dark:text-slate-200 placeholder-slate-400 dark:placeholder-slate-500 resize-none focus:outline-none focus:ring-1 focus:ring-amber-400 dark:focus:ring-amber-600"
|
|
293
|
+
rows={2}
|
|
294
|
+
onClick={(e) => e.stopPropagation()}
|
|
295
|
+
/>
|
|
296
|
+
<button
|
|
297
|
+
onClick={(e) => { e.stopPropagation(); handleSaveNote(); }}
|
|
298
|
+
className="absolute top-1 right-1 p-0.5 hover:bg-amber-200 dark:hover:bg-amber-800 rounded text-amber-600 dark:text-amber-400"
|
|
299
|
+
title="Done (Enter)"
|
|
300
|
+
type="button"
|
|
301
|
+
>
|
|
302
|
+
<Check size={10} />
|
|
303
|
+
</button>
|
|
304
|
+
</div>
|
|
305
|
+
)}
|
|
306
|
+
</div>
|
|
307
|
+
</div>
|
|
308
|
+
)}
|
|
309
|
+
</div>
|
|
310
|
+
);
|
|
311
|
+
});
|
|
@@ -1,271 +0,0 @@
|
|
|
1
|
-
import React, { useState, useCallback, useMemo } from 'react';
|
|
2
|
-
import {
|
|
3
|
-
Activity,
|
|
4
|
-
Check,
|
|
5
|
-
ChevronLeft,
|
|
6
|
-
ChevronRight,
|
|
7
|
-
FolderOpen,
|
|
8
|
-
MessageCircle,
|
|
9
|
-
Moon,
|
|
10
|
-
Pencil,
|
|
11
|
-
Sun,
|
|
12
|
-
} from 'lucide-react';
|
|
13
|
-
import type { AnnotationData, SessionSummary } from '../../types';
|
|
14
|
-
import { formatTimestamp } from '../../utils';
|
|
15
|
-
import { Tooltip } from '../common/Tooltip';
|
|
16
|
-
|
|
17
|
-
export const SessionsSidebar: React.FC<{
|
|
18
|
-
width: number;
|
|
19
|
-
isCollapsed: boolean;
|
|
20
|
-
setIsCollapsed: (v: boolean) => void;
|
|
21
|
-
onStartResize: (e: React.MouseEvent) => void;
|
|
22
|
-
|
|
23
|
-
sessionList: SessionSummary[];
|
|
24
|
-
selectedSessionId: string | null;
|
|
25
|
-
onSelectSession: (sessionId: string) => void;
|
|
26
|
-
|
|
27
|
-
isDarkMode: boolean;
|
|
28
|
-
onToggleTheme: () => void;
|
|
29
|
-
|
|
30
|
-
annotations: Record<string, AnnotationData>;
|
|
31
|
-
onUpdateSessionNote: (sessionId: string, note: string) => void;
|
|
32
|
-
}> = ({
|
|
33
|
-
width,
|
|
34
|
-
isCollapsed,
|
|
35
|
-
setIsCollapsed,
|
|
36
|
-
onStartResize,
|
|
37
|
-
sessionList,
|
|
38
|
-
selectedSessionId,
|
|
39
|
-
onSelectSession,
|
|
40
|
-
isDarkMode,
|
|
41
|
-
onToggleTheme,
|
|
42
|
-
annotations,
|
|
43
|
-
onUpdateSessionNote,
|
|
44
|
-
}) => {
|
|
45
|
-
const [editingSessionNote, setEditingSessionNote] = useState<string | null>(null);
|
|
46
|
-
|
|
47
|
-
// Memoize callback functions
|
|
48
|
-
const handleToggleCollapse = useCallback(() => {
|
|
49
|
-
setIsCollapsed(!isCollapsed);
|
|
50
|
-
}, [isCollapsed, setIsCollapsed]);
|
|
51
|
-
|
|
52
|
-
// Memoize rendered sessions
|
|
53
|
-
const renderedSessions = useMemo(() => {
|
|
54
|
-
return sessionList.map((session) => {
|
|
55
|
-
const sessionNote = annotations[session.id]?.session_note || '';
|
|
56
|
-
const hasNote = sessionNote.length > 0;
|
|
57
|
-
const isEditing = editingSessionNote === session.id;
|
|
58
|
-
const isSelected = selectedSessionId === session.id;
|
|
59
|
-
|
|
60
|
-
const handleSelectSession = useCallback(() => {
|
|
61
|
-
onSelectSession(session.id);
|
|
62
|
-
if (isCollapsed) setIsCollapsed(false);
|
|
63
|
-
}, [session.id, onSelectSession, isCollapsed, setIsCollapsed]);
|
|
64
|
-
|
|
65
|
-
const handleToggleEdit = useCallback(() => {
|
|
66
|
-
setEditingSessionNote(isEditing ? null : session.id);
|
|
67
|
-
}, [isEditing, session.id]);
|
|
68
|
-
|
|
69
|
-
const handleUpdateNote = useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
|
70
|
-
onUpdateSessionNote(session.id, e.target.value);
|
|
71
|
-
}, [session.id, onUpdateSessionNote]);
|
|
72
|
-
|
|
73
|
-
const handleKeyDown = useCallback((e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
|
74
|
-
if (e.key === 'Escape') {
|
|
75
|
-
setEditingSessionNote(null);
|
|
76
|
-
} else if (e.key === 'Enter' && !e.shiftKey) {
|
|
77
|
-
e.preventDefault();
|
|
78
|
-
setEditingSessionNote(null);
|
|
79
|
-
}
|
|
80
|
-
}, []);
|
|
81
|
-
|
|
82
|
-
const handleSaveNote = useCallback(() => {
|
|
83
|
-
setEditingSessionNote(null);
|
|
84
|
-
}, []);
|
|
85
|
-
|
|
86
|
-
const handleEditNote = useCallback(() => {
|
|
87
|
-
setEditingSessionNote(session.id);
|
|
88
|
-
}, [session.id]);
|
|
89
|
-
|
|
90
|
-
return (
|
|
91
|
-
<div key={session.id}>
|
|
92
|
-
{/* Collapsed state */}
|
|
93
|
-
{isCollapsed ? (
|
|
94
|
-
<div
|
|
95
|
-
onClick={handleSelectSession}
|
|
96
|
-
className={`h-12 flex items-center justify-center cursor-pointer border-b border-gray-100 dark:border-slate-800/50 relative rounded-lg mb-1 ${
|
|
97
|
-
isSelected ? 'bg-blue-50 dark:bg-slate-800' : ''
|
|
98
|
-
}`}
|
|
99
|
-
>
|
|
100
|
-
<FolderOpen
|
|
101
|
-
size={20}
|
|
102
|
-
className={
|
|
103
|
-
isSelected
|
|
104
|
-
? 'text-blue-600 dark:text-blue-400'
|
|
105
|
-
: 'text-slate-400 dark:text-slate-600'
|
|
106
|
-
}
|
|
107
|
-
/>
|
|
108
|
-
{hasNote && <div className="absolute top-1 right-1 w-2 h-2 bg-amber-500 rounded-full"></div>}
|
|
109
|
-
</div>
|
|
110
|
-
) : (
|
|
111
|
-
<div>
|
|
112
|
-
<div
|
|
113
|
-
onClick={handleSelectSession}
|
|
114
|
-
className={`px-3 py-3 border-b border-gray-100 dark:border-slate-800/50 cursor-pointer transition-colors group relative rounded-lg mb-1 ${
|
|
115
|
-
isSelected ? 'bg-blue-50 dark:bg-slate-800/80 shadow-md z-10' : 'hover:bg-gray-50 dark:hover:bg-slate-800/30'
|
|
116
|
-
}`}
|
|
117
|
-
title={session.id}
|
|
118
|
-
>
|
|
119
|
-
{/* Action Buttons (appear on hover) */}
|
|
120
|
-
<div className="absolute right-2 top-2 flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity z-20">
|
|
121
|
-
<button
|
|
122
|
-
onClick={(e) => { e.stopPropagation(); handleToggleEdit(); }}
|
|
123
|
-
className="p-1.5 bg-white dark:bg-slate-700 rounded shadow-sm hover:scale-110"
|
|
124
|
-
title="Edit note"
|
|
125
|
-
type="button"
|
|
126
|
-
>
|
|
127
|
-
<Pencil size={12} className="text-slate-500 dark:text-slate-300" />
|
|
128
|
-
</button>
|
|
129
|
-
</div>
|
|
130
|
-
|
|
131
|
-
{/* Selection indicator */}
|
|
132
|
-
{isSelected && (
|
|
133
|
-
<div className="absolute left-0 top-3 bottom-3 w-1 bg-blue-500 rounded-r-full"></div>
|
|
134
|
-
)}
|
|
135
|
-
|
|
136
|
-
<div className="flex items-center justify-between mb-1">
|
|
137
|
-
<div className="flex items-center gap-2">
|
|
138
|
-
<FolderOpen
|
|
139
|
-
size={16}
|
|
140
|
-
className={
|
|
141
|
-
isSelected
|
|
142
|
-
? 'text-blue-600 dark:text-blue-400'
|
|
143
|
-
: 'text-slate-400 dark:text-slate-600'
|
|
144
|
-
}
|
|
145
|
-
/>
|
|
146
|
-
<span className="font-semibold truncate max-w-[120px] text-slate-700 dark:text-slate-200" title={session.id}>
|
|
147
|
-
{session.id.replace('session_', '')}
|
|
148
|
-
</span>
|
|
149
|
-
</div>
|
|
150
|
-
<div className="flex items-center gap-1 text-xs text-slate-400 dark:text-slate-500">
|
|
151
|
-
<span>{session.request_count}</span>
|
|
152
|
-
{hasNote && !isEditing && <MessageCircle size={10} className="text-amber-500" />}
|
|
153
|
-
</div>
|
|
154
|
-
</div>
|
|
155
|
-
<div className="text-xs text-slate-400 dark:text-slate-500">
|
|
156
|
-
{formatTimestamp(session.timestamp)}
|
|
157
|
-
</div>
|
|
158
|
-
|
|
159
|
-
{/* Note Section - inside the card */}
|
|
160
|
-
{!isEditing && hasNote && (
|
|
161
|
-
<Tooltip text={sessionNote}>
|
|
162
|
-
<div
|
|
163
|
-
className="mt-2 px-2 py-1 text-[10px] text-amber-700 dark:text-amber-300 bg-amber-50 dark:bg-amber-950/20 rounded border-l-2 border-amber-400 dark:border-amber-600 cursor-pointer hover:bg-amber-100 dark:hover:bg-amber-950/30 transition-colors truncate"
|
|
164
|
-
onClick={(e) => { e.stopPropagation(); handleEditNote(); }}
|
|
165
|
-
title="Click to edit"
|
|
166
|
-
>
|
|
167
|
-
{sessionNote}
|
|
168
|
-
</div>
|
|
169
|
-
</Tooltip>
|
|
170
|
-
)}
|
|
171
|
-
|
|
172
|
-
{/* Note Editor - inside the card */}
|
|
173
|
-
{isEditing && (
|
|
174
|
-
<div className="mt-2 relative">
|
|
175
|
-
<textarea
|
|
176
|
-
autoFocus
|
|
177
|
-
value={sessionNote}
|
|
178
|
-
onChange={handleUpdateNote}
|
|
179
|
-
onKeyDown={handleKeyDown}
|
|
180
|
-
onBlur={handleSaveNote}
|
|
181
|
-
placeholder="Add a note..."
|
|
182
|
-
className="w-full text-xs p-1.5 pr-6 border border-amber-300 dark:border-amber-700 rounded bg-amber-50 dark:bg-amber-950/30 text-slate-700 dark:text-slate-200 placeholder-slate-400 dark:placeholder-slate-500 resize-none focus:outline-none focus:ring-1 focus:ring-amber-400 dark:focus:ring-amber-600"
|
|
183
|
-
rows={2}
|
|
184
|
-
onClick={(e) => e.stopPropagation()}
|
|
185
|
-
/>
|
|
186
|
-
<button
|
|
187
|
-
onClick={(e) => { e.stopPropagation(); handleSaveNote(); }}
|
|
188
|
-
className="absolute top-1 right-1 p-0.5 hover:bg-amber-200 dark:hover:bg-amber-800 rounded text-amber-600 dark:text-amber-400"
|
|
189
|
-
title="Done (Enter)"
|
|
190
|
-
type="button"
|
|
191
|
-
>
|
|
192
|
-
<Check size={10} />
|
|
193
|
-
</button>
|
|
194
|
-
</div>
|
|
195
|
-
)}
|
|
196
|
-
</div>
|
|
197
|
-
</div>
|
|
198
|
-
)}
|
|
199
|
-
</div>
|
|
200
|
-
);
|
|
201
|
-
});
|
|
202
|
-
}, [
|
|
203
|
-
sessionList,
|
|
204
|
-
annotations,
|
|
205
|
-
editingSessionNote,
|
|
206
|
-
selectedSessionId,
|
|
207
|
-
isCollapsed,
|
|
208
|
-
onSelectSession,
|
|
209
|
-
setIsCollapsed,
|
|
210
|
-
onUpdateSessionNote,
|
|
211
|
-
]);
|
|
212
|
-
|
|
213
|
-
return (
|
|
214
|
-
<div
|
|
215
|
-
style={{ width: isCollapsed ? '48px' : width }}
|
|
216
|
-
className="flex-shrink-0 border-r border-gray-200 dark:border-slate-800 bg-white dark:bg-[#0b1120] flex flex-col relative transition-all duration-300 ease-in-out"
|
|
217
|
-
>
|
|
218
|
-
{/* Sessions Header */}
|
|
219
|
-
<div
|
|
220
|
-
className={`p-4 border-b border-gray-200 dark:border-slate-800 flex items-center ${
|
|
221
|
-
isCollapsed ? 'justify-center flex-col gap-4' : 'justify-between'
|
|
222
|
-
}`}
|
|
223
|
-
>
|
|
224
|
-
{!isCollapsed && (
|
|
225
|
-
<div className="flex items-center gap-2">
|
|
226
|
-
<Activity size={18} className="text-blue-600 dark:text-blue-500" />
|
|
227
|
-
<h2 className="font-bold text-sm tracking-wide text-slate-700 dark:text-slate-200">
|
|
228
|
-
SESSIONS
|
|
229
|
-
</h2>
|
|
230
|
-
</div>
|
|
231
|
-
)}
|
|
232
|
-
|
|
233
|
-
<div className={`flex ${isCollapsed ? 'flex-col gap-3' : 'gap-1'}`}>
|
|
234
|
-
<button
|
|
235
|
-
onClick={handleToggleCollapse}
|
|
236
|
-
className="p-1.5 hover:bg-gray-100 dark:hover:bg-slate-800 rounded text-slate-500 dark:text-slate-400 transition-colors"
|
|
237
|
-
title={isCollapsed ? 'Expand' : 'Collapse'}
|
|
238
|
-
type="button"
|
|
239
|
-
>
|
|
240
|
-
{isCollapsed ? <ChevronRight size={14} /> : <ChevronLeft size={14} />}
|
|
241
|
-
</button>
|
|
242
|
-
{!isCollapsed && (
|
|
243
|
-
<button
|
|
244
|
-
onClick={onToggleTheme}
|
|
245
|
-
className="p-1.5 hover:bg-gray-100 dark:hover:bg-slate-800 rounded text-slate-500 dark:text-slate-400 transition-colors"
|
|
246
|
-
title="Toggle Theme"
|
|
247
|
-
type="button"
|
|
248
|
-
>
|
|
249
|
-
{isDarkMode ? <Sun size={14} /> : <Moon size={14} />}
|
|
250
|
-
</button>
|
|
251
|
-
)}
|
|
252
|
-
</div>
|
|
253
|
-
</div>
|
|
254
|
-
|
|
255
|
-
{/* Sessions List */}
|
|
256
|
-
<div className="overflow-y-auto flex-1 p-2 space-y-1 custom-scrollbar">
|
|
257
|
-
{renderedSessions}
|
|
258
|
-
</div>
|
|
259
|
-
|
|
260
|
-
{/* Resizer Handle */}
|
|
261
|
-
{!isCollapsed && (
|
|
262
|
-
<div
|
|
263
|
-
className="absolute top-0 right-0 w-1 h-full cursor-col-resize hover:bg-blue-500/50 transition-colors z-10 flex items-center justify-center group"
|
|
264
|
-
onMouseDown={onStartResize}
|
|
265
|
-
>
|
|
266
|
-
<div className="w-[1px] h-full bg-gray-200 dark:bg-slate-800 group-hover:bg-blue-500"></div>
|
|
267
|
-
</div>
|
|
268
|
-
)}
|
|
269
|
-
</div>
|
|
270
|
-
);
|
|
271
|
-
};
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/ui/src/components/layout/ExchangeDetailsPane.tsx
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|