apteva 0.4.4 → 0.4.5

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.
@@ -1,6 +1,7 @@
1
1
  import React, { useState, useEffect } from "react";
2
2
  import { CheckIcon, CloseIcon, PlusIcon } from "../common/Icons";
3
3
  import { Modal, useConfirm } from "../common/Modal";
4
+ import { Select } from "../common/Select";
4
5
  import { useProjects, useAuth, type Project } from "../../context";
5
6
  import type { Provider } from "../../types";
6
7
 
@@ -91,6 +92,7 @@ function SettingsNavItem({
91
92
 
92
93
  function ProvidersSettings() {
93
94
  const { authFetch } = useAuth();
95
+ const { projects, projectsEnabled } = useProjects();
94
96
  const [providers, setProviders] = useState<Provider[]>([]);
95
97
  const [selectedProvider, setSelectedProvider] = useState<string | null>(null);
96
98
  const [apiKey, setApiKey] = useState("");
@@ -213,7 +215,7 @@ function ProvidersSettings() {
213
215
  </p>
214
216
  </div>
215
217
 
216
- <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
218
+ <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
217
219
  {llmProviders.map(provider => (
218
220
  <ProviderKeyCard
219
221
  key={provider.id}
@@ -251,7 +253,7 @@ function ProvidersSettings() {
251
253
  </p>
252
254
  </div>
253
255
 
254
- <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
256
+ <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
255
257
  {integrations.map(provider => (
256
258
  <IntegrationKeyCard
257
259
  key={provider.id}
@@ -275,6 +277,9 @@ function ProvidersSettings() {
275
277
  onApiKeyChange={setApiKey}
276
278
  onSave={saveKey}
277
279
  onDelete={() => deleteKey(provider.id)}
280
+ projectsEnabled={projectsEnabled}
281
+ projects={projects}
282
+ onRefresh={fetchProviders}
278
283
  />
279
284
  ))}
280
285
  </div>
@@ -932,6 +937,22 @@ function ProviderKeyCard({
932
937
  );
933
938
  }
934
939
 
940
+ interface IntegrationKey {
941
+ id: string;
942
+ provider_id: string;
943
+ key_hint: string;
944
+ is_valid: boolean;
945
+ project_id: string | null;
946
+ name: string | null;
947
+ created_at: string;
948
+ }
949
+
950
+ interface IntegrationKeyCardProps extends ProviderKeyCardProps {
951
+ projectsEnabled: boolean;
952
+ projects: Array<{ id: string; name: string; color: string }>;
953
+ onRefresh: () => void;
954
+ }
955
+
935
956
  function IntegrationKeyCard({
936
957
  provider,
937
958
  isEditing,
@@ -945,20 +966,195 @@ function IntegrationKeyCard({
945
966
  onApiKeyChange,
946
967
  onSave,
947
968
  onDelete,
948
- }: ProviderKeyCardProps) {
969
+ projectsEnabled,
970
+ projects,
971
+ onRefresh,
972
+ }: IntegrationKeyCardProps) {
973
+ const { authFetch } = useAuth();
974
+ const [keys, setKeys] = useState<IntegrationKey[]>([]);
975
+ const [selectedProjectId, setSelectedProjectId] = useState<string>("");
976
+ const [expanded, setExpanded] = useState(false);
977
+ const { confirm, ConfirmDialog } = useConfirm();
978
+
979
+ // Fetch all keys for this provider
980
+ const fetchKeys = async () => {
981
+ try {
982
+ const res = await authFetch(`/api/keys/${provider.id}`);
983
+ const data = await res.json();
984
+ setKeys(data.keys || []);
985
+ } catch (e) {
986
+ console.error("Failed to fetch keys:", e);
987
+ }
988
+ };
989
+
990
+ useEffect(() => {
991
+ if (projectsEnabled) {
992
+ fetchKeys();
993
+ }
994
+ }, [provider.id, projectsEnabled]);
995
+
996
+ const handleSaveWithProject = async () => {
997
+ if (!apiKey) return;
998
+
999
+ try {
1000
+ const res = await authFetch(`/api/keys/${provider.id}`, {
1001
+ method: "POST",
1002
+ headers: { "Content-Type": "application/json" },
1003
+ body: JSON.stringify({
1004
+ key: apiKey,
1005
+ project_id: selectedProjectId || null,
1006
+ }),
1007
+ });
1008
+
1009
+ if (res.ok) {
1010
+ onApiKeyChange("");
1011
+ setSelectedProjectId("");
1012
+ onCancelEdit();
1013
+ fetchKeys();
1014
+ onRefresh();
1015
+ }
1016
+ } catch (e) {
1017
+ console.error("Failed to save key:", e);
1018
+ }
1019
+ };
1020
+
1021
+ const handleDeleteKey = async (keyId: string, keyName: string | null) => {
1022
+ const confirmed = await confirm(
1023
+ `Are you sure you want to remove this API key${keyName ? ` (${keyName})` : ""}?`,
1024
+ { confirmText: "Remove", title: "Remove API Key" }
1025
+ );
1026
+ if (!confirmed) return;
1027
+
1028
+ try {
1029
+ await authFetch(`/api/keys/by-id/${keyId}`, { method: "DELETE" });
1030
+ fetchKeys();
1031
+ onRefresh();
1032
+ } catch (e) {
1033
+ console.error("Failed to delete key:", e);
1034
+ }
1035
+ };
1036
+
1037
+ const globalKey = keys.find(k => !k.project_id);
1038
+ const projectKeys = keys.filter(k => k.project_id);
1039
+ const getProjectName = (projectId: string) => projects.find(p => p.id === projectId)?.name || "Unknown";
1040
+ const getProjectColor = (projectId: string) => projects.find(p => p.id === projectId)?.color || "#666";
1041
+
1042
+ // Simple view when projects not enabled
1043
+ if (!projectsEnabled) {
1044
+ return (
1045
+ <div className={`bg-[#111] border rounded-lg p-4 ${
1046
+ provider.hasKey ? 'border-[#f97316]/20' : 'border-[#1a1a1a]'
1047
+ }`}>
1048
+ <div className="flex items-center justify-between mb-2">
1049
+ <div>
1050
+ <h3 className="font-medium">{provider.name}</h3>
1051
+ <p className="text-sm text-[#666]">{provider.description || "MCP integration"}</p>
1052
+ </div>
1053
+ {provider.hasKey ? (
1054
+ <span className="text-[#f97316] text-xs flex items-center gap-1 bg-[#f97316]/10 px-2 py-1 rounded">
1055
+ <CheckIcon className="w-3 h-3" />
1056
+ {provider.keyHint}
1057
+ </span>
1058
+ ) : (
1059
+ <span className="text-[#666] text-xs bg-[#1a1a1a] px-2 py-1 rounded">
1060
+ Not configured
1061
+ </span>
1062
+ )}
1063
+ </div>
1064
+
1065
+ <div className="mt-3 pt-3 border-t border-[#1a1a1a]">
1066
+ {isEditing ? (
1067
+ <div className="space-y-3">
1068
+ <input
1069
+ type="password"
1070
+ value={apiKey}
1071
+ onChange={e => onApiKeyChange(e.target.value)}
1072
+ placeholder={provider.hasKey ? "Enter new API key..." : "Enter API key..."}
1073
+ autoFocus
1074
+ className="w-full bg-[#0a0a0a] border border-[#333] rounded px-3 py-2 focus:outline-none focus:border-[#f97316]"
1075
+ />
1076
+ {error && <p className="text-red-400 text-sm">{error}</p>}
1077
+ {success && <p className="text-green-400 text-sm">{success}</p>}
1078
+ <div className="flex gap-2">
1079
+ <button
1080
+ onClick={onCancelEdit}
1081
+ className="flex-1 px-3 py-1.5 border border-[#333] rounded text-sm hover:border-[#666]"
1082
+ >
1083
+ Cancel
1084
+ </button>
1085
+ <button
1086
+ onClick={onSave}
1087
+ disabled={!apiKey || saving}
1088
+ className="flex-1 px-3 py-1.5 bg-[#f97316] text-black rounded text-sm font-medium disabled:opacity-50"
1089
+ >
1090
+ {testing ? "Validating..." : saving ? "Saving..." : "Save"}
1091
+ </button>
1092
+ </div>
1093
+ </div>
1094
+ ) : provider.hasKey ? (
1095
+ <div className="flex items-center justify-between">
1096
+ <a
1097
+ href={provider.docsUrl}
1098
+ target="_blank"
1099
+ rel="noopener noreferrer"
1100
+ className="text-sm text-[#3b82f6] hover:underline"
1101
+ >
1102
+ View docs
1103
+ </a>
1104
+ <div className="flex items-center gap-3">
1105
+ <button
1106
+ onClick={onStartEdit}
1107
+ className="text-sm text-[#888] hover:text-[#e0e0e0]"
1108
+ >
1109
+ Update key
1110
+ </button>
1111
+ <button
1112
+ onClick={onDelete}
1113
+ className="text-red-400 hover:text-red-300 text-sm"
1114
+ >
1115
+ Remove
1116
+ </button>
1117
+ </div>
1118
+ </div>
1119
+ ) : (
1120
+ <div className="flex items-center justify-between">
1121
+ <a
1122
+ href={provider.docsUrl}
1123
+ target="_blank"
1124
+ rel="noopener noreferrer"
1125
+ className="text-sm text-[#3b82f6] hover:underline"
1126
+ >
1127
+ Get API key
1128
+ </a>
1129
+ <button
1130
+ onClick={onStartEdit}
1131
+ className="text-sm text-[#f97316] hover:text-[#fb923c]"
1132
+ >
1133
+ + Add key
1134
+ </button>
1135
+ </div>
1136
+ )}
1137
+ </div>
1138
+ </div>
1139
+ );
1140
+ }
1141
+
1142
+ // Enhanced view with project support
949
1143
  return (
1144
+ <>
1145
+ {ConfirmDialog}
950
1146
  <div className={`bg-[#111] border rounded-lg p-4 ${
951
- provider.hasKey ? 'border-[#f97316]/20' : 'border-[#1a1a1a]'
1147
+ keys.length > 0 ? 'border-[#f97316]/20' : 'border-[#1a1a1a]'
952
1148
  }`}>
953
1149
  <div className="flex items-center justify-between mb-2">
954
1150
  <div>
955
1151
  <h3 className="font-medium">{provider.name}</h3>
956
1152
  <p className="text-sm text-[#666]">{provider.description || "MCP integration"}</p>
957
1153
  </div>
958
- {provider.hasKey ? (
1154
+ {keys.length > 0 ? (
959
1155
  <span className="text-[#f97316] text-xs flex items-center gap-1 bg-[#f97316]/10 px-2 py-1 rounded">
960
1156
  <CheckIcon className="w-3 h-3" />
961
- {provider.keyHint}
1157
+ {keys.length} key{keys.length !== 1 ? "s" : ""}
962
1158
  </span>
963
1159
  ) : (
964
1160
  <span className="text-[#666] text-xs bg-[#1a1a1a] px-2 py-1 rounded">
@@ -967,6 +1163,58 @@ function IntegrationKeyCard({
967
1163
  )}
968
1164
  </div>
969
1165
 
1166
+ {/* Keys List */}
1167
+ {keys.length > 0 && (
1168
+ <div className="mt-3 space-y-2">
1169
+ {/* Global Key */}
1170
+ {globalKey && (
1171
+ <div className="flex items-center justify-between text-sm bg-[#0a0a0a] rounded px-3 py-2">
1172
+ <div className="flex items-center gap-2">
1173
+ <span className="text-[#888]">Global</span>
1174
+ <span className="text-[#555]">·</span>
1175
+ <span className="text-[#666] font-mono text-xs">{globalKey.key_hint}</span>
1176
+ </div>
1177
+ <button
1178
+ onClick={() => handleDeleteKey(globalKey.id, "Global")}
1179
+ className="text-red-400 hover:text-red-300 text-xs"
1180
+ >
1181
+ Remove
1182
+ </button>
1183
+ </div>
1184
+ )}
1185
+
1186
+ {/* Project Keys - show first 2, expand for more */}
1187
+ {projectKeys.slice(0, expanded ? undefined : 2).map(key => (
1188
+ <div key={key.id} className="flex items-center justify-between text-sm bg-[#0a0a0a] rounded px-3 py-2">
1189
+ <div className="flex items-center gap-2 min-w-0">
1190
+ <span
1191
+ className="w-2 h-2 rounded-full flex-shrink-0"
1192
+ style={{ backgroundColor: getProjectColor(key.project_id!) }}
1193
+ />
1194
+ <span className="text-[#888] truncate">{key.name || getProjectName(key.project_id!)}</span>
1195
+ <span className="text-[#555]">·</span>
1196
+ <span className="text-[#666] font-mono text-xs">{key.key_hint}</span>
1197
+ </div>
1198
+ <button
1199
+ onClick={() => handleDeleteKey(key.id, key.name || getProjectName(key.project_id!))}
1200
+ className="text-red-400 hover:text-red-300 text-xs flex-shrink-0 ml-2"
1201
+ >
1202
+ Remove
1203
+ </button>
1204
+ </div>
1205
+ ))}
1206
+
1207
+ {projectKeys.length > 2 && !expanded && (
1208
+ <button
1209
+ onClick={() => setExpanded(true)}
1210
+ className="text-xs text-[#666] hover:text-[#888] w-full text-center py-1"
1211
+ >
1212
+ Show {projectKeys.length - 2} more...
1213
+ </button>
1214
+ )}
1215
+ </div>
1216
+ )}
1217
+
970
1218
  <div className="mt-3 pt-3 border-t border-[#1a1a1a]">
971
1219
  {isEditing ? (
972
1220
  <div className="space-y-3">
@@ -974,50 +1222,40 @@ function IntegrationKeyCard({
974
1222
  type="password"
975
1223
  value={apiKey}
976
1224
  onChange={e => onApiKeyChange(e.target.value)}
977
- placeholder={provider.hasKey ? "Enter new API key..." : "Enter API key..."}
1225
+ placeholder="Enter API key..."
978
1226
  autoFocus
979
1227
  className="w-full bg-[#0a0a0a] border border-[#333] rounded px-3 py-2 focus:outline-none focus:border-[#f97316]"
980
1228
  />
1229
+
1230
+ <Select
1231
+ value={selectedProjectId}
1232
+ onChange={setSelectedProjectId}
1233
+ placeholder="Global (all projects)"
1234
+ options={[
1235
+ { value: "", label: "Global (all projects)" },
1236
+ ...projects.map(p => ({ value: p.id, label: p.name }))
1237
+ ]}
1238
+ />
1239
+
981
1240
  {error && <p className="text-red-400 text-sm">{error}</p>}
982
1241
  {success && <p className="text-green-400 text-sm">{success}</p>}
1242
+
983
1243
  <div className="flex gap-2">
984
1244
  <button
985
- onClick={onCancelEdit}
1245
+ onClick={() => {
1246
+ onCancelEdit();
1247
+ setSelectedProjectId("");
1248
+ }}
986
1249
  className="flex-1 px-3 py-1.5 border border-[#333] rounded text-sm hover:border-[#666]"
987
1250
  >
988
1251
  Cancel
989
1252
  </button>
990
1253
  <button
991
- onClick={onSave}
1254
+ onClick={handleSaveWithProject}
992
1255
  disabled={!apiKey || saving}
993
1256
  className="flex-1 px-3 py-1.5 bg-[#f97316] text-black rounded text-sm font-medium disabled:opacity-50"
994
1257
  >
995
- {testing ? "Validating..." : saving ? "Saving..." : "Save"}
996
- </button>
997
- </div>
998
- </div>
999
- ) : provider.hasKey ? (
1000
- <div className="flex items-center justify-between">
1001
- <a
1002
- href={provider.docsUrl}
1003
- target="_blank"
1004
- rel="noopener noreferrer"
1005
- className="text-sm text-[#3b82f6] hover:underline"
1006
- >
1007
- View docs
1008
- </a>
1009
- <div className="flex items-center gap-3">
1010
- <button
1011
- onClick={onStartEdit}
1012
- className="text-sm text-[#888] hover:text-[#e0e0e0]"
1013
- >
1014
- Update key
1015
- </button>
1016
- <button
1017
- onClick={onDelete}
1018
- className="text-red-400 hover:text-red-300 text-sm"
1019
- >
1020
- Remove
1258
+ {saving ? "Saving..." : "Save"}
1021
1259
  </button>
1022
1260
  </div>
1023
1261
  </div>
@@ -1029,7 +1267,7 @@ function IntegrationKeyCard({
1029
1267
  rel="noopener noreferrer"
1030
1268
  className="text-sm text-[#3b82f6] hover:underline"
1031
1269
  >
1032
- Get API key
1270
+ {keys.length > 0 ? "View docs" : "Get API key"}
1033
1271
  </a>
1034
1272
  <button
1035
1273
  onClick={onStartEdit}
@@ -1041,6 +1279,7 @@ function IntegrationKeyCard({
1041
1279
  )}
1042
1280
  </div>
1043
1281
  </div>
1282
+ </>
1044
1283
  );
1045
1284
  }
1046
1285