File size: 9,851 Bytes
2868f80
f5628ad
2868f80
f5628ad
e3e8fd0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f5628ad
 
 
 
 
 
ee2b23c
d203cbe
 
e3e8fd0
 
 
 
6360789
 
 
f5628ad
2868f80
 
 
 
 
 
e3e8fd0
2868f80
9ec69a0
2868f80
 
 
 
 
 
 
f5628ad
 
 
e3e8fd0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f5628ad
 
 
 
 
 
 
 
 
 
 
 
 
e3e8fd0
f5628ad
 
d203cbe
 
 
 
 
 
 
 
 
6694d87
d203cbe
 
 
 
 
 
6694d87
d203cbe
 
6694d87
d203cbe
 
6694d87
d203cbe
6694d87
d203cbe
 
 
 
 
 
 
 
 
f5628ad
 
6360789
 
 
 
 
 
 
 
 
 
 
 
 
 
f5628ad
 
 
 
6360789
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2868f80
6360789
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f5628ad
 
 
 
9962311
2868f80
0c01b28
 
 
 
 
9962311
 
 
 
0c01b28
9962311
 
 
 
 
0c01b28
9962311
f5628ad
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
import { useState } from "react";
import FileUpload from "./FileUpload";
import { deleteDocument } from "../api";

function NewWorkspaceInput({ onCreated }) {
  const [name, setName] = useState("");
  const [open, setOpen] = useState(false);

  function handleCreate(e) {
    e.preventDefault();
    const slug = name
      .trim()
      .toLowerCase()
      .replace(/\s+/g, "-")
      .replace(/[^a-z0-9-]/g, "");
    if (!slug) return;
    onCreated(slug);
    setName("");
    setOpen(false);
  }

  if (!open) {
    return (
      <button
        type="button"
        onClick={() => setOpen(true)}
        className="mt-2 w-full text-xs text-indigo-500 hover:text-indigo-700 text-left"
      >
        + New workspace
      </button>
    );
  }

  return (
    <form onSubmit={handleCreate} className="mt-2 flex gap-1">
      <input
        autoFocus
        value={name}
        onChange={(e) => setName(e.target.value)}
        placeholder="workspace-name"
        className="flex-1 text-xs border border-gray-200 rounded-lg px-2 py-1.5 focus:outline-none focus:ring-1 focus:ring-indigo-500"
      />
      <button
        type="submit"
        className="text-xs bg-indigo-600 text-white rounded-lg px-2 py-1.5 hover:bg-indigo-700"
      >
        Create
      </button>
    </form>
  );
}


export default function Sidebar({
  documents,
  setDocuments,
  onNewConversation,
  evalLog,
  onBriefing,
  briefing,
  onSuggestedQuestion,
  currentWorkspace,
  workspaces,
  onWorkspaceChange,
  onWorkspacesUpdate,
  filterDocs = [],
  onFilterChange,
  onFilterClear,
}) {
  const [deletingDoc, setDeletingDoc] = useState(null);

  async function handleDelete(docName) {
    if (!window.confirm(`Remove "${docName}" from the index?`)) return;
    setDeletingDoc(docName);
    try {
      const result = await deleteDocument(docName, currentWorkspace);
      setDocuments(result.documents);
      if (filterDocs.includes(docName)) onFilterChange?.(docName);
    } catch (e) {
      alert(`Delete failed: ${e?.response?.data?.detail || e.message}`);
    } finally {
      setDeletingDoc(null);
    }
  }

  return (
    <aside className="w-80 bg-white border-r border-gray-100 flex flex-col shrink-0 h-[calc(100vh-65px)] overflow-y-auto">
      <div className="p-4 space-y-4">
        {/* Workspace switcher */}
        <div className="bg-gray-50/80 rounded-xl p-3.5">
          <h3 className="text-xs font-semibold text-indigo-500 uppercase tracking-wider mb-2">
            Workspace
          </h3>
          <select
            value={currentWorkspace}
            onChange={(e) => onWorkspaceChange(e.target.value)}
            className="w-full text-sm border border-gray-200 rounded-lg px-3 py-2 bg-white focus:outline-none focus:ring-2 focus:ring-indigo-500"
          >
            {workspaces.map((ws) => (
              <option key={ws} value={ws}>
                {ws}
              </option>
            ))}
          </select>
          <NewWorkspaceInput
            onCreated={(ws) => {
              onWorkspacesUpdate((prev) => [...new Set([...prev, ws])]);
              onWorkspaceChange(ws);
            }}
          />
        </div>

        {/* New Conversation */}
        <button
          onClick={onNewConversation}
          className="w-full px-4 py-2.5 bg-indigo-600 text-white rounded-xl text-sm font-medium hover:bg-indigo-700 transition-colors shadow-sm"
        >
          New Conversation
        </button>

        {/* File Upload */}
        <div className="bg-gray-50/80 rounded-xl p-3.5">
          <h3 className="text-xs font-semibold text-indigo-500 uppercase tracking-wider mb-2.5">
            Upload Documents
          </h3>
          <FileUpload onUploadComplete={setDocuments} onBriefing={onBriefing} currentWorkspace={currentWorkspace} />
        </div>

        {/* Briefing card */}
        {briefing && (
          <div className="bg-indigo-50 border border-indigo-100 rounded-xl p-3.5 space-y-2">
            <h3 className="text-xs font-semibold text-indigo-500 uppercase tracking-wider truncate">
              {briefing.doc_name.length > 30
                ? briefing.doc_name.slice(0, 30) + "…"
                : briefing.doc_name}
            </h3>
            <ul className="space-y-1">
              {(briefing.summary ?? []).map((point, i) => (
                <li key={i} className="text-xs text-gray-600 flex gap-1.5">
                  <span className="text-indigo-400 shrink-0"></span>
                  {point}
                </li>
              ))}
            </ul>
            {(briefing.suggested_questions ?? []).length > 0 && (
              <div className="pt-1 space-y-1">
                <p className="text-xs text-indigo-400 font-medium">Try asking:</p>
                {(briefing.suggested_questions ?? []).map((q, i) => (
                  <button
                    key={i}
                    type="button"
                    onClick={() => onSuggestedQuestion(q)}
                    className="block w-full text-left text-xs bg-white border border-indigo-200 rounded-lg px-2.5 py-1.5 text-gray-700 hover:bg-indigo-50 hover:border-indigo-400 transition-colors focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:outline-none"
                  >
                    {q}
                  </button>
                ))}
              </div>
            )}
          </div>
        )}

        {/* Uploaded Documents */}
        <div className="bg-gray-50/80 rounded-xl p-3.5">
          <div className="flex items-center justify-between mb-2.5">
            <h3 className="text-xs font-semibold text-indigo-500 uppercase tracking-wider">
              Documents ({documents.length})
            </h3>
            {filterDocs.length > 0 && (
              <button
                type="button"
                onClick={onFilterClear}
                className="text-xs text-indigo-400 hover:text-indigo-600 transition-colors"
              >
                Clear filter
              </button>
            )}
          </div>
          {documents.length === 0 ? (
            <p className="text-sm text-gray-400">No documents yet</p>
          ) : (
            <ul className="space-y-1.5">
              {documents.map((doc, i) => {
                const isSelected = filterDocs.includes(doc.name);
                const isFiltering = filterDocs.length > 0;
                return (
                  <li
                    key={i}
                    className={`flex items-center justify-between text-sm bg-white rounded-lg px-3 py-2 shadow-xs transition-all cursor-pointer ${
                      isSelected
                        ? "ring-2 ring-indigo-500 bg-indigo-50"
                        : isFiltering
                        ? "opacity-50"
                        : "hover:ring-1 hover:ring-indigo-300"
                    }`}
                    onClick={() => onFilterChange(doc.name)}
                    title={isSelected ? "Click to remove filter" : "Click to filter to this doc"}
                  >
                    <span className={`truncate flex-1 min-w-0 ${isSelected ? "text-indigo-700 font-medium" : "text-gray-700"}`}>
                      {doc.name}
                    </span>
                    <span className="text-indigo-400 shrink-0 ml-2 text-xs font-medium">
                      {doc.chunk_count}
                    </span>
                    <button
                      onClick={(e) => { e.stopPropagation(); handleDelete(doc.name); }}
                      disabled={deletingDoc === doc.name}
                      title="Remove document"
                      className="ml-2 shrink-0 text-gray-300 hover:text-red-500 transition-colors disabled:opacity-40"
                    >
                      {deletingDoc === doc.name ? (
                        <svg className="w-3.5 h-3.5 animate-spin" viewBox="0 0 24 24" fill="none">
                          <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"/>
                          <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v8z"/>
                        </svg>
                      ) : (
                        <svg className="w-3.5 h-3.5" viewBox="0 0 20 20" fill="currentColor">
                          <path fillRule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zm-1 6a1 1 0 112 0v5a1 1 0 11-2 0V8zm4 0a1 1 0 112 0v5a1 1 0 11-2 0V8z" clipRule="evenodd"/>
                        </svg>
                      )}
                    </button>
                  </li>
                );
              })}
            </ul>
          )}
        </div>

      </div>

      {/* Footer: version badge + eval link */}
      <div className="mt-auto p-4 border-t border-gray-100 flex items-center justify-between">
        <span className="text-xs text-gray-400 font-medium">
          <span className="bg-indigo-50 text-indigo-600 px-2 py-0.5 rounded-full font-semibold">Violet v1.3</span>
        </span>
        <a
          href="https://askprism-eval.vercel.app/"
          target="_blank"
          rel="noopener noreferrer"
          className="flex items-center gap-1.5 text-xs text-gray-400 hover:text-indigo-600 transition-colors group"
        >
          <svg className="w-3.5 h-3.5 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
            <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2"
              d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
          </svg>
          <span className="group-hover:underline">Eval →</span>
        </a>
      </div>
    </aside>
  );
}