apteva 0.3.9 → 0.4.1

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.
@@ -4,7 +4,7 @@ import { Modal, useConfirm } from "../common/Modal";
4
4
  import { useProjects, useAuth, type Project } from "../../context";
5
5
  import type { Provider } from "../../types";
6
6
 
7
- type SettingsTab = "providers" | "projects" | "updates" | "data";
7
+ type SettingsTab = "providers" | "projects" | "account" | "updates" | "data";
8
8
 
9
9
  export function SettingsPage() {
10
10
  const { projectsEnabled } = useProjects();
@@ -13,6 +13,7 @@ export function SettingsPage() {
13
13
  const tabs: { key: SettingsTab; label: string }[] = [
14
14
  { key: "providers", label: "Providers" },
15
15
  ...(projectsEnabled ? [{ key: "projects" as SettingsTab, label: "Projects" }] : []),
16
+ { key: "account", label: "Account" },
16
17
  { key: "updates", label: "Updates" },
17
18
  { key: "data", label: "Data" },
18
19
  ];
@@ -57,6 +58,7 @@ export function SettingsPage() {
57
58
  <div className="flex-1 overflow-auto p-4 md:p-6">
58
59
  {activeTab === "providers" && <ProvidersSettings />}
59
60
  {activeTab === "projects" && projectsEnabled && <ProjectsSettings />}
61
+ {activeTab === "account" && <AccountSettings />}
60
62
  {activeTab === "updates" && <UpdatesSettings />}
61
63
  {activeTab === "data" && <DataSettings />}
62
64
  </div>
@@ -793,6 +795,19 @@ function ProviderKeyCard({
793
795
  onSave,
794
796
  onDelete,
795
797
  }: ProviderKeyCardProps) {
798
+ const isOllama = provider.id === "ollama";
799
+ const [ollamaStatus, setOllamaStatus] = React.useState<{ connected: boolean; modelCount?: number } | null>(null);
800
+
801
+ // Check Ollama status when configured
802
+ React.useEffect(() => {
803
+ if (isOllama && provider.hasKey) {
804
+ fetch("/api/providers/ollama/status")
805
+ .then(res => res.json())
806
+ .then(data => setOllamaStatus({ connected: data.connected, modelCount: data.modelCount }))
807
+ .catch(() => setOllamaStatus({ connected: false }));
808
+ }
809
+ }, [isOllama, provider.hasKey]);
810
+
796
811
  return (
797
812
  <div className={`bg-[#111] border rounded-lg p-4 ${
798
813
  provider.hasKey ? 'border-green-500/20' : 'border-[#1a1a1a]'
@@ -803,13 +818,28 @@ function ProviderKeyCard({
803
818
  <p className="text-sm text-[#666] truncate">
804
819
  {provider.type === "integration"
805
820
  ? (provider.description || "MCP integration")
806
- : `${provider.models.length} models`}
821
+ : isOllama
822
+ ? "Run models locally"
823
+ : `${provider.models.length} models`}
807
824
  </p>
808
825
  </div>
809
826
  {provider.hasKey ? (
810
- <span className="text-green-400 text-xs flex items-center gap-1 bg-green-500/10 px-2 py-1 rounded whitespace-nowrap flex-shrink-0">
811
- <CheckIcon className="w-3 h-3" />
812
- {provider.keyHint}
827
+ <span className={`text-xs flex items-center gap-1 px-2 py-1 rounded whitespace-nowrap flex-shrink-0 ${
828
+ isOllama && ollamaStatus
829
+ ? ollamaStatus.connected
830
+ ? "text-green-400 bg-green-500/10"
831
+ : "text-yellow-400 bg-yellow-500/10"
832
+ : "text-green-400 bg-green-500/10"
833
+ }`}>
834
+ {isOllama && ollamaStatus ? (
835
+ ollamaStatus.connected ? (
836
+ <><CheckIcon className="w-3 h-3" />{ollamaStatus.modelCount} models</>
837
+ ) : (
838
+ <>Not running</>
839
+ )
840
+ ) : (
841
+ <><CheckIcon className="w-3 h-3" />{provider.keyHint}</>
842
+ )}
813
843
  </span>
814
844
  ) : (
815
845
  <span className="text-[#666] text-xs bg-[#1a1a1a] px-2 py-1 rounded whitespace-nowrap flex-shrink-0">
@@ -822,13 +852,20 @@ function ProviderKeyCard({
822
852
  {isEditing ? (
823
853
  <div className="space-y-3">
824
854
  <input
825
- type="password"
855
+ type={isOllama ? "text" : "password"}
826
856
  value={apiKey}
827
857
  onChange={e => onApiKeyChange(e.target.value)}
828
- placeholder={provider.hasKey ? "Enter new API key..." : "Enter API key..."}
858
+ placeholder={isOllama
859
+ ? "http://localhost:11434"
860
+ : provider.hasKey ? "Enter new API key..." : "Enter API key..."}
829
861
  autoFocus
830
862
  className="w-full bg-[#0a0a0a] border border-[#333] rounded px-3 py-2 focus:outline-none focus:border-[#f97316]"
831
863
  />
864
+ {isOllama && (
865
+ <p className="text-xs text-[#666]">
866
+ Enter your Ollama server URL. Default is http://localhost:11434
867
+ </p>
868
+ )}
832
869
  {error && <p className="text-red-400 text-sm">{error}</p>}
833
870
  {success && <p className="text-green-400 text-sm">{success}</p>}
834
871
  <div className="flex gap-2">
@@ -843,7 +880,7 @@ function ProviderKeyCard({
843
880
  disabled={!apiKey || saving}
844
881
  className="flex-1 px-3 py-1.5 bg-[#f97316] text-black rounded text-sm font-medium disabled:opacity-50"
845
882
  >
846
- {testing ? "Validating..." : saving ? "Saving..." : "Save"}
883
+ {testing ? "Validating..." : saving ? "Saving..." : isOllama ? "Connect" : "Save"}
847
884
  </button>
848
885
  </div>
849
886
  </div>
@@ -855,14 +892,14 @@ function ProviderKeyCard({
855
892
  rel="noopener noreferrer"
856
893
  className="text-sm text-[#3b82f6] hover:underline"
857
894
  >
858
- View docs
895
+ {isOllama ? "Download Ollama" : "View docs"}
859
896
  </a>
860
897
  <div className="flex items-center gap-3">
861
898
  <button
862
899
  onClick={onStartEdit}
863
900
  className="text-sm text-[#888] hover:text-[#e0e0e0]"
864
901
  >
865
- Update key
902
+ {isOllama ? "Change URL" : "Update key"}
866
903
  </button>
867
904
  <button
868
905
  onClick={onDelete}
@@ -880,13 +917,13 @@ function ProviderKeyCard({
880
917
  rel="noopener noreferrer"
881
918
  className="text-sm text-[#3b82f6] hover:underline"
882
919
  >
883
- Get API key
920
+ {isOllama ? "Download Ollama" : "Get API key"}
884
921
  </a>
885
922
  <button
886
923
  onClick={onStartEdit}
887
924
  className="text-sm text-[#f97316] hover:text-[#fb923c]"
888
925
  >
889
- + Add key
926
+ {isOllama ? "Configure" : "+ Add key"}
890
927
  </button>
891
928
  </div>
892
929
  )}
@@ -1007,6 +1044,146 @@ function IntegrationKeyCard({
1007
1044
  );
1008
1045
  }
1009
1046
 
1047
+ function AccountSettings() {
1048
+ const { authFetch, user } = useAuth();
1049
+ const [currentPassword, setCurrentPassword] = useState("");
1050
+ const [newPassword, setNewPassword] = useState("");
1051
+ const [confirmPassword, setConfirmPassword] = useState("");
1052
+ const [saving, setSaving] = useState(false);
1053
+ const [message, setMessage] = useState<{ type: "success" | "error"; text: string } | null>(null);
1054
+
1055
+ const handleChangePassword = async () => {
1056
+ // Validation
1057
+ if (!currentPassword || !newPassword || !confirmPassword) {
1058
+ setMessage({ type: "error", text: "All fields are required" });
1059
+ return;
1060
+ }
1061
+
1062
+ if (newPassword !== confirmPassword) {
1063
+ setMessage({ type: "error", text: "New passwords do not match" });
1064
+ return;
1065
+ }
1066
+
1067
+ if (newPassword.length < 8) {
1068
+ setMessage({ type: "error", text: "Password must be at least 8 characters" });
1069
+ return;
1070
+ }
1071
+
1072
+ setSaving(true);
1073
+ setMessage(null);
1074
+
1075
+ try {
1076
+ const res = await authFetch("/api/auth/password", {
1077
+ method: "PUT",
1078
+ headers: { "Content-Type": "application/json" },
1079
+ body: JSON.stringify({ currentPassword, newPassword }),
1080
+ });
1081
+
1082
+ const data = await res.json();
1083
+
1084
+ if (res.ok) {
1085
+ setMessage({ type: "success", text: "Password updated successfully" });
1086
+ setCurrentPassword("");
1087
+ setNewPassword("");
1088
+ setConfirmPassword("");
1089
+ } else {
1090
+ setMessage({ type: "error", text: data.error || "Failed to update password" });
1091
+ }
1092
+ } catch {
1093
+ setMessage({ type: "error", text: "Failed to update password" });
1094
+ }
1095
+
1096
+ setSaving(false);
1097
+ };
1098
+
1099
+ return (
1100
+ <div className="max-w-4xl w-full">
1101
+ <div className="mb-6">
1102
+ <h1 className="text-2xl font-semibold mb-1">Account Settings</h1>
1103
+ <p className="text-[#666]">Manage your account and security.</p>
1104
+ </div>
1105
+
1106
+ {/* User Info */}
1107
+ {user && (
1108
+ <div className="bg-[#111] border border-[#1a1a1a] rounded-lg p-4 mb-6">
1109
+ <h3 className="font-medium mb-3">Profile</h3>
1110
+ <div className="space-y-2 text-sm">
1111
+ <div className="flex justify-between">
1112
+ <span className="text-[#666]">Username</span>
1113
+ <span>{user.username}</span>
1114
+ </div>
1115
+ {user.email && (
1116
+ <div className="flex justify-between">
1117
+ <span className="text-[#666]">Email</span>
1118
+ <span>{user.email}</span>
1119
+ </div>
1120
+ )}
1121
+ <div className="flex justify-between">
1122
+ <span className="text-[#666]">Role</span>
1123
+ <span className="capitalize">{user.role}</span>
1124
+ </div>
1125
+ </div>
1126
+ </div>
1127
+ )}
1128
+
1129
+ {/* Change Password */}
1130
+ <div className="bg-[#111] border border-[#1a1a1a] rounded-lg p-4">
1131
+ <h3 className="font-medium mb-4">Change Password</h3>
1132
+
1133
+ <div className="space-y-4 max-w-md">
1134
+ <div>
1135
+ <label className="block text-sm text-[#666] mb-1">Current Password</label>
1136
+ <input
1137
+ type="password"
1138
+ value={currentPassword}
1139
+ onChange={(e) => setCurrentPassword(e.target.value)}
1140
+ className="w-full bg-[#0a0a0a] border border-[#333] rounded px-3 py-2 focus:outline-none focus:border-[#f97316]"
1141
+ />
1142
+ </div>
1143
+
1144
+ <div>
1145
+ <label className="block text-sm text-[#666] mb-1">New Password</label>
1146
+ <input
1147
+ type="password"
1148
+ value={newPassword}
1149
+ onChange={(e) => setNewPassword(e.target.value)}
1150
+ className="w-full bg-[#0a0a0a] border border-[#333] rounded px-3 py-2 focus:outline-none focus:border-[#f97316]"
1151
+ />
1152
+ </div>
1153
+
1154
+ <div>
1155
+ <label className="block text-sm text-[#666] mb-1">Confirm New Password</label>
1156
+ <input
1157
+ type="password"
1158
+ value={confirmPassword}
1159
+ onChange={(e) => setConfirmPassword(e.target.value)}
1160
+ className="w-full bg-[#0a0a0a] border border-[#333] rounded px-3 py-2 focus:outline-none focus:border-[#f97316]"
1161
+ />
1162
+ </div>
1163
+
1164
+ {message && (
1165
+ <div className={`p-3 rounded text-sm ${
1166
+ message.type === "success"
1167
+ ? "bg-green-500/10 text-green-400 border border-green-500/30"
1168
+ : "bg-red-500/10 text-red-400 border border-red-500/30"
1169
+ }`}>
1170
+ {message.text}
1171
+ </div>
1172
+ )}
1173
+
1174
+ <button
1175
+ onClick={handleChangePassword}
1176
+ disabled={saving || !currentPassword || !newPassword || !confirmPassword}
1177
+ className="px-4 py-2 bg-[#f97316] hover:bg-[#fb923c] disabled:opacity-50 disabled:cursor-not-allowed text-black rounded text-sm font-medium transition"
1178
+ >
1179
+ {saving ? "Updating..." : "Update Password"}
1180
+ </button>
1181
+ </div>
1182
+ </div>
1183
+ </div>
1184
+ );
1185
+ }
1186
+
1010
1187
  function DataSettings() {
1011
1188
  const { authFetch } = useAuth();
1012
1189
  const [clearing, setClearing] = useState(false);