SCGR commited on
Commit
4f6cbcc
·
1 Parent(s): a8153d0

dynamic prompt management

Browse files
frontend/src/pages/AdminPage/AdminPage.module.css CHANGED
@@ -93,6 +93,28 @@
93
  margin-bottom: var(--go-ui-spacing-xl);
94
  }
95
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
  .modelsTable table {
97
  width: 100%;
98
  border-collapse: collapse;
 
93
  margin-bottom: var(--go-ui-spacing-xl);
94
  }
95
 
96
+ /* Prompt subsections */
97
+ .promptSubsection {
98
+ margin-bottom: var(--go-ui-spacing-2xl);
99
+ padding: var(--go-ui-spacing-lg);
100
+ background-color: var(--go-ui-color-gray-5);
101
+ border-radius: var(--go-ui-border-radius-lg);
102
+ border: var(--go-ui-width-separator-thin) solid var(--go-ui-color-gray-20);
103
+ }
104
+
105
+ .promptSubsection:last-child {
106
+ margin-bottom: 0;
107
+ }
108
+
109
+ .promptSubsectionTitle {
110
+ font-size: var(--go-ui-font-size-lg);
111
+ font-weight: var(--go-ui-font-weight-semibold);
112
+ color: var(--go-ui-color-gray-90);
113
+ margin: 0 0 var(--go-ui-spacing-lg) 0;
114
+ padding-bottom: var(--go-ui-spacing-sm);
115
+ border-bottom: var(--go-ui-width-separator-thin) solid var(--go-ui-color-gray-30);
116
+ }
117
+
118
  .modelsTable table {
119
  width: 100%;
120
  border-collapse: collapse;
frontend/src/pages/AdminPage/AdminPage.tsx CHANGED
@@ -33,15 +33,26 @@ export default function AdminPage() {
33
  p_code: string;
34
  label: string;
35
  metadata_instructions?: string;
 
 
 
 
 
 
 
36
  }>>([]);
37
 
38
  // Prompt management state
39
  const [showEditPromptForm, setShowEditPromptForm] = useState(false);
 
 
40
  const [editingPrompt, setEditingPrompt] = useState<any>(null);
41
  const [newPromptData, setNewPromptData] = useState({
42
  p_code: '',
43
  label: '',
44
- metadata_instructions: ''
 
 
45
  });
46
 
47
  // Model management state
@@ -72,6 +83,7 @@ export default function AdminPage() {
72
  if (isAuthenticated) {
73
  fetchModels();
74
  fetchPrompts();
 
75
  }
76
  }, [isAuthenticated]);
77
 
@@ -113,12 +125,26 @@ export default function AdminPage() {
113
  });
114
  };
115
 
 
 
 
 
 
 
 
 
 
 
 
 
116
  const handleEditPrompt = (prompt: any) => {
117
  setEditingPrompt(prompt);
118
  setNewPromptData({
119
  p_code: prompt.p_code,
120
  label: prompt.label || '',
121
- metadata_instructions: prompt.metadata_instructions || ''
 
 
122
  });
123
  setShowEditPromptForm(true);
124
  };
@@ -132,7 +158,9 @@ export default function AdminPage() {
132
  },
133
  body: JSON.stringify({
134
  label: newPromptData.label,
135
- metadata_instructions: newPromptData.metadata_instructions
 
 
136
  }),
137
  });
138
 
@@ -141,7 +169,7 @@ export default function AdminPage() {
141
  fetchPrompts();
142
  setShowEditPromptForm(false);
143
  setEditingPrompt(null);
144
- setNewPromptData({ p_code: '', label: '', metadata_instructions: '' });
145
  } else {
146
  const errorData = await response.json();
147
  alert(`Failed to update prompt: ${errorData.error || 'Unknown error'}`);
@@ -151,6 +179,70 @@ export default function AdminPage() {
151
  }
152
  };
153
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
154
  const toggleModelAvailability = async (modelCode: string, currentStatus: boolean) => {
155
  try {
156
  const response = await fetch(`/api/models/${modelCode}/toggle`, {
@@ -731,72 +823,159 @@ Model "${newModelData.label}" added successfully!
731
  </div>
732
  </Container>
733
 
734
- {/* Prompts Section */}
735
  <Container
736
- heading="Prompts Management"
737
  headingLevel={2}
738
  withHeaderBorder
739
  withInternalPadding
740
  >
741
  <div className={styles.modelManagementArea}>
742
- {/* Prompts Table */}
743
- <div className={styles.modelsTable}>
744
- <table>
745
- <thead>
746
- <tr>
747
- <th>Code</th>
748
- <th>Label</th>
749
- <th>Actions</th>
750
- </tr>
751
- </thead>
752
- <tbody>
753
- {availablePrompts.map(prompt => (
754
- <tr key={prompt.p_code}>
755
- <td className={styles.modelCode}>{prompt.p_code}</td>
756
- <td className={styles.promptLabel}>{prompt.label || 'No label'}</td>
757
- <td>
758
- <div className={styles.modelActions}>
 
 
 
 
 
759
  <Button
760
- name={`view-${prompt.p_code}`}
761
- variant="secondary"
762
  size={1}
763
- onClick={() => {
764
- setTestResults(`=== Prompt Details ===\nCode: ${prompt.p_code}\nLabel: ${prompt.label}\n\nMetadata Instructions:\n${prompt.metadata_instructions || 'No instructions available'}`);
765
- setTestResultsTitle(`Prompt: ${prompt.p_code}`);
766
- setShowTestResultsModal(true);
767
- }}
768
  >
769
- View
770
  </Button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
771
  <Button
772
- name={`edit-${prompt.p_code}`}
773
- variant="secondary"
774
  size={1}
775
- onClick={() => handleEditPrompt(prompt)}
776
  >
777
- Edit
778
  </Button>
779
- </div>
780
- </td>
781
- </tr>
782
- ))}
783
- </tbody>
784
- </table>
785
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
786
 
787
- {/* Add Prompt Button */}
788
- <div className={styles.addModelButtonContainer}>
789
- <Button
790
- name="add-prompt"
791
- variant="primary"
792
- onClick={() => {
793
- // TODO: Implement add prompt functionality
794
- alert('Add prompt functionality coming soon!');
795
- }}
796
- >
797
- Add New Prompt
798
- </Button>
799
  </div>
 
800
  </div>
801
  </Container>
802
 
@@ -991,6 +1170,28 @@ Model "${newModelData.label}" added successfully!
991
  className={styles.formInput}
992
  />
993
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
994
  <div className={styles.formField}>
995
  <label className={styles.formLabel}>Metadata Instructions:</label>
996
  <textarea
@@ -1009,7 +1210,7 @@ Model "${newModelData.label}" added successfully!
1009
  onClick={() => {
1010
  setShowEditPromptForm(false);
1011
  setEditingPrompt(null);
1012
- setNewPromptData({ p_code: '', label: '', metadata_instructions: '' });
1013
  }}
1014
  >
1015
  Cancel
@@ -1026,6 +1227,94 @@ Model "${newModelData.label}" added successfully!
1026
  </div>
1027
  </div>
1028
  )}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1029
  </PageContainer>
1030
  );
1031
  }
 
33
  p_code: string;
34
  label: string;
35
  metadata_instructions?: string;
36
+ image_type: string;
37
+ is_active: boolean;
38
+ }>>([]);
39
+
40
+ const [imageTypes, setImageTypes] = useState<Array<{
41
+ image_type: string;
42
+ label: string;
43
  }>>([]);
44
 
45
  // Prompt management state
46
  const [showEditPromptForm, setShowEditPromptForm] = useState(false);
47
+ const [showAddPromptForm, setShowAddPromptForm] = useState(false);
48
+ const [addingPromptType, setAddingPromptType] = useState<'crisis_map' | 'drone_image' | null>(null);
49
  const [editingPrompt, setEditingPrompt] = useState<any>(null);
50
  const [newPromptData, setNewPromptData] = useState({
51
  p_code: '',
52
  label: '',
53
+ metadata_instructions: '',
54
+ image_type: 'crisis_map',
55
+ is_active: false
56
  });
57
 
58
  // Model management state
 
83
  if (isAuthenticated) {
84
  fetchModels();
85
  fetchPrompts();
86
+ fetchImageTypes();
87
  }
88
  }, [isAuthenticated]);
89
 
 
125
  });
126
  };
127
 
128
+ const fetchImageTypes = () => {
129
+ fetch('/api/image-types')
130
+ .then(r => r.json())
131
+ .then(imageTypesData => {
132
+ console.log('Image types data received:', imageTypesData);
133
+ setImageTypes(imageTypesData || []);
134
+ })
135
+ .catch(() => {
136
+ // Handle error silently
137
+ });
138
+ };
139
+
140
  const handleEditPrompt = (prompt: any) => {
141
  setEditingPrompt(prompt);
142
  setNewPromptData({
143
  p_code: prompt.p_code,
144
  label: prompt.label || '',
145
+ metadata_instructions: prompt.metadata_instructions || '',
146
+ image_type: prompt.image_type || 'crisis_map',
147
+ is_active: prompt.is_active || false
148
  });
149
  setShowEditPromptForm(true);
150
  };
 
158
  },
159
  body: JSON.stringify({
160
  label: newPromptData.label,
161
+ metadata_instructions: newPromptData.metadata_instructions,
162
+ image_type: newPromptData.image_type,
163
+ is_active: newPromptData.is_active
164
  }),
165
  });
166
 
 
169
  fetchPrompts();
170
  setShowEditPromptForm(false);
171
  setEditingPrompt(null);
172
+ setNewPromptData({ p_code: '', label: '', metadata_instructions: '', image_type: 'crisis_map', is_active: false });
173
  } else {
174
  const errorData = await response.json();
175
  alert(`Failed to update prompt: ${errorData.error || 'Unknown error'}`);
 
179
  }
180
  };
181
 
182
+ const handleTogglePromptActive = async (promptCode: string, imageType: string) => {
183
+ try {
184
+ const response = await fetch(`/api/prompts/${promptCode}/toggle-active?image_type=${imageType}`, {
185
+ method: 'POST',
186
+ headers: {
187
+ 'Content-Type': 'application/json',
188
+ },
189
+ });
190
+
191
+ if (response.ok) {
192
+ // Refresh prompts to show updated status
193
+ fetchPrompts();
194
+ } else {
195
+ const errorData = await response.json();
196
+ alert(`Failed to toggle prompt active status: ${errorData.detail || 'Unknown error'}`);
197
+ }
198
+ } catch (error) {
199
+ alert('Error toggling prompt active status');
200
+ }
201
+ };
202
+
203
+ const handleAddPrompt = (imageType: 'crisis_map' | 'drone_image') => {
204
+ setAddingPromptType(imageType);
205
+ setNewPromptData({
206
+ p_code: '',
207
+ label: '',
208
+ metadata_instructions: '',
209
+ image_type: imageType,
210
+ is_active: false
211
+ });
212
+ setShowAddPromptForm(true);
213
+ };
214
+
215
+ const handleSaveNewPrompt = async () => {
216
+ try {
217
+ const response = await fetch('/api/prompts', {
218
+ method: 'POST',
219
+ headers: {
220
+ 'Content-Type': 'application/json',
221
+ },
222
+ body: JSON.stringify(newPromptData),
223
+ });
224
+
225
+ if (response.ok) {
226
+ // Refresh prompts and close form
227
+ fetchPrompts();
228
+ setShowAddPromptForm(false);
229
+ setAddingPromptType(null);
230
+ setNewPromptData({
231
+ p_code: '',
232
+ label: '',
233
+ metadata_instructions: '',
234
+ image_type: 'crisis_map',
235
+ is_active: false
236
+ });
237
+ } else {
238
+ const errorData = await response.json();
239
+ alert(`Failed to create prompt: ${errorData.detail || 'Unknown error'}`);
240
+ }
241
+ } catch (error) {
242
+ alert(`Error creating prompt: ${error}`);
243
+ }
244
+ };
245
+
246
  const toggleModelAvailability = async (modelCode: string, currentStatus: boolean) => {
247
  try {
248
  const response = await fetch(`/api/models/${modelCode}/toggle`, {
 
823
  </div>
824
  </Container>
825
 
826
+ {/* Prompts Management Section */}
827
  <Container
828
+ heading="Prompt Management"
829
  headingLevel={2}
830
  withHeaderBorder
831
  withInternalPadding
832
  >
833
  <div className={styles.modelManagementArea}>
834
+
835
+ {/* Crisis Maps Sub-section */}
836
+ <div className={styles.promptSubsection}>
837
+ <h4 className={styles.promptSubsectionTitle}>Crisis Maps</h4>
838
+ <div className={styles.modelsTable}>
839
+ <table>
840
+ <thead>
841
+ <tr>
842
+ <th>Code</th>
843
+ <th>Label</th>
844
+ <th>Status</th>
845
+ <th>Actions</th>
846
+ </tr>
847
+ </thead>
848
+ <tbody>
849
+ {availablePrompts
850
+ .filter(prompt => prompt.image_type === 'crisis_map')
851
+ .map(prompt => (
852
+ <tr key={prompt.p_code}>
853
+ <td className={styles.modelCode}>{prompt.p_code}</td>
854
+ <td className={styles.promptLabel}>{prompt.label || 'No label'}</td>
855
+ <td>
856
  <Button
857
+ name={`toggle-crisis-${prompt.p_code}`}
858
+ variant={prompt.is_active ? "primary" : "secondary"}
859
  size={1}
860
+ onClick={() => handleTogglePromptActive(prompt.p_code, 'crisis_map')}
 
 
 
 
861
  >
862
+ {prompt.is_active ? 'Active' : 'Inactive'}
863
  </Button>
864
+ </td>
865
+ <td>
866
+ <div className={styles.modelActions}>
867
+ <Button
868
+ name={`view-${prompt.p_code}`}
869
+ variant="secondary"
870
+ size={1}
871
+ onClick={() => {
872
+ setTestResults(`=== Prompt Details ===\nCode: ${prompt.p_code}\nLabel: ${prompt.label}\nImage Type: ${prompt.image_type}\nActive: ${prompt.is_active}\n\nMetadata Instructions:\n${prompt.metadata_instructions || 'No instructions available'}`);
873
+ setTestResultsTitle(`Prompt: ${prompt.p_code}`);
874
+ setShowTestResultsModal(true);
875
+ }}
876
+ >
877
+ View
878
+ </Button>
879
+ <Button
880
+ name={`edit-${prompt.p_code}`}
881
+ variant="secondary"
882
+ size={1}
883
+ onClick={() => handleEditPrompt(prompt)}
884
+ >
885
+ Edit
886
+ </Button>
887
+ </div>
888
+ </td>
889
+ </tr>
890
+ ))}
891
+ </tbody>
892
+ </table>
893
+ </div>
894
+
895
+ {/* Add Crisis Map Prompt Button */}
896
+ <div className={styles.addModelButtonContainer}>
897
+ <Button
898
+ name="add-crisis-prompt"
899
+ variant="primary"
900
+ onClick={() => handleAddPrompt('crisis_map')}
901
+ >
902
+ Add New Crisis Map Prompt
903
+ </Button>
904
+ </div>
905
+ </div>
906
+
907
+ {/* Drone Images Sub-section */}
908
+ <div className={styles.promptSubsection}>
909
+ <h4 className={styles.promptSubsectionTitle}>Drone Images</h4>
910
+ <div className={styles.modelsTable}>
911
+ <table>
912
+ <thead>
913
+ <tr>
914
+ <th>Code</th>
915
+ <th>Label</th>
916
+ <th>Status</th>
917
+ <th>Actions</th>
918
+ </tr>
919
+ </thead>
920
+ <tbody>
921
+ {availablePrompts
922
+ .filter(prompt => prompt.image_type === 'drone_image')
923
+ .map(prompt => (
924
+ <tr key={prompt.p_code}>
925
+ <td className={styles.modelCode}>{prompt.p_code}</td>
926
+ <td className={styles.promptLabel}>{prompt.label || 'No label'}</td>
927
+ <td>
928
  <Button
929
+ name={`toggle-drone-${prompt.p_code}`}
930
+ variant={prompt.is_active ? "primary" : "secondary"}
931
  size={1}
932
+ onClick={() => handleTogglePromptActive(prompt.p_code, 'drone_image')}
933
  >
934
+ {prompt.is_active ? 'Active' : 'Inactive'}
935
  </Button>
936
+ </td>
937
+ <td>
938
+ <div className={styles.modelActions}>
939
+ <Button
940
+ name={`view-${prompt.p_code}`}
941
+ variant="secondary"
942
+ size={1}
943
+ onClick={() => {
944
+ setTestResults(`=== Prompt Details ===\nCode: ${prompt.p_code}\nLabel: ${prompt.label}\nImage Type: ${prompt.image_type}\nActive: ${prompt.is_active}\n\nMetadata Instructions:\n${prompt.metadata_instructions || 'No instructions available'}`);
945
+ setTestResultsTitle(`Prompt: ${prompt.p_code}`);
946
+ setShowTestResultsModal(true);
947
+ }}
948
+ >
949
+ View
950
+ </Button>
951
+ <Button
952
+ name={`edit-${prompt.p_code}`}
953
+ variant="secondary"
954
+ size={1}
955
+ onClick={() => handleEditPrompt(prompt)}
956
+ >
957
+ Edit
958
+ </Button>
959
+ </div>
960
+ </td>
961
+ </tr>
962
+ ))}
963
+ </tbody>
964
+ </table>
965
+ </div>
966
 
967
+ {/* Add Drone Image Prompt Button */}
968
+ <div className={styles.addModelButtonContainer}>
969
+ <Button
970
+ name="add-drone-prompt"
971
+ variant="primary"
972
+ onClick={() => handleAddPrompt('drone_image')}
973
+ >
974
+ Add New Drone Image Prompt
975
+ </Button>
976
+ </div>
 
 
977
  </div>
978
+
979
  </div>
980
  </Container>
981
 
 
1170
  className={styles.formInput}
1171
  />
1172
  </div>
1173
+ <div className={styles.formField}>
1174
+ <label className={styles.formLabel}>Image Type:</label>
1175
+ <SelectInput
1176
+ name="prompt-image-type"
1177
+ value={newPromptData.image_type}
1178
+ onChange={(value) => setNewPromptData(prev => ({ ...prev, image_type: value || 'crisis_map' }))}
1179
+ options={imageTypes}
1180
+ keySelector={(o) => o.image_type}
1181
+ labelSelector={(o) => o.label}
1182
+ />
1183
+ </div>
1184
+ <div className={styles.formField}>
1185
+ <label className={styles.formLabel}>Active Status:</label>
1186
+ <div className={styles.addModelFormCheckbox}>
1187
+ <input
1188
+ type="checkbox"
1189
+ checked={newPromptData.is_active}
1190
+ onChange={(e) => setNewPromptData(prev => ({ ...prev, is_active: e.target.checked }))}
1191
+ />
1192
+ <span>Active for this image type</span>
1193
+ </div>
1194
+ </div>
1195
  <div className={styles.formField}>
1196
  <label className={styles.formLabel}>Metadata Instructions:</label>
1197
  <textarea
 
1210
  onClick={() => {
1211
  setShowEditPromptForm(false);
1212
  setEditingPrompt(null);
1213
+ setNewPromptData({ p_code: '', label: '', metadata_instructions: '', image_type: 'crisis_map', is_active: false });
1214
  }}
1215
  >
1216
  Cancel
 
1227
  </div>
1228
  </div>
1229
  )}
1230
+
1231
+ {/* Add Prompt Form Modal */}
1232
+ {showAddPromptForm && (
1233
+ <div className={styles.modalOverlay} onClick={() => setShowAddPromptForm(false)}>
1234
+ <div className={styles.modalContent} onClick={(e) => e.stopPropagation()}>
1235
+ <div className={styles.modalBody}>
1236
+ <h3 className={styles.modalTitle}>
1237
+ Add New {addingPromptType === 'crisis_map' ? 'Crisis Map' : 'Drone Image'} Prompt
1238
+ </h3>
1239
+ <div className={styles.modalForm}>
1240
+ <div className={styles.formField}>
1241
+ <label className={styles.formLabel}>Code:</label>
1242
+ <TextInput
1243
+ name="prompt-code"
1244
+ value={newPromptData.p_code}
1245
+ onChange={(value) => setNewPromptData(prev => ({ ...prev, p_code: value || '' }))}
1246
+ placeholder="e.g., CUSTOM_CRISIS_MAP_001"
1247
+ className={styles.formInput}
1248
+ />
1249
+ </div>
1250
+ <div className={styles.formField}>
1251
+ <label className={styles.formLabel}>Label:</label>
1252
+ <TextInput
1253
+ name="prompt-label"
1254
+ value={newPromptData.label}
1255
+ onChange={(value) => setNewPromptData(prev => ({ ...prev, label: value || '' }))}
1256
+ placeholder="Enter prompt description..."
1257
+ className={styles.formInput}
1258
+ />
1259
+ </div>
1260
+ <div className={styles.formField}>
1261
+ <label className={styles.formLabel}>Image Type:</label>
1262
+ <TextInput
1263
+ name="prompt-image-type"
1264
+ value={newPromptData.image_type}
1265
+ onChange={() => {}} // Disabled - automatically set
1266
+ disabled={true}
1267
+ className={styles.formInput}
1268
+ />
1269
+ </div>
1270
+ <div className={styles.formField}>
1271
+ <label className={styles.formLabel}>Active Status:</label>
1272
+ <div className={styles.addModelFormCheckbox}>
1273
+ <input
1274
+ type="checkbox"
1275
+ checked={newPromptData.is_active}
1276
+ onChange={(e) => setNewPromptData(prev => ({ ...prev, is_active: e.target.checked }))}
1277
+ />
1278
+ <span>Active for this image type</span>
1279
+ </div>
1280
+ </div>
1281
+ <div className={styles.formField}>
1282
+ <label className={styles.formLabel}>Metadata Instructions:</label>
1283
+ <textarea
1284
+ name="prompt-instructions"
1285
+ value={newPromptData.metadata_instructions}
1286
+ onChange={(e) => setNewPromptData(prev => ({ ...prev, metadata_instructions: e.target.value }))}
1287
+ placeholder="Enter metadata extraction instructions..."
1288
+ className={`${styles.formInput} ${styles.textarea}`}
1289
+ rows={8}
1290
+ />
1291
+ </div>
1292
+ </div>
1293
+ <div className={styles.modalButtons}>
1294
+ <Button
1295
+ name="cancel-add-prompt"
1296
+ variant="tertiary"
1297
+ onClick={() => {
1298
+ setShowAddPromptForm(false);
1299
+ setAddingPromptType(null);
1300
+ setNewPromptData({ p_code: '', label: '', metadata_instructions: '', image_type: 'crisis_map', is_active: false });
1301
+ }}
1302
+ >
1303
+ Cancel
1304
+ </Button>
1305
+ <Button
1306
+ name="save-new-prompt"
1307
+ variant="primary"
1308
+ onClick={handleSaveNewPrompt}
1309
+ disabled={!newPromptData.p_code || !newPromptData.label}
1310
+ >
1311
+ Create Prompt
1312
+ </Button>
1313
+ </div>
1314
+ </div>
1315
+ </div>
1316
+ </div>
1317
+ )}
1318
  </PageContainer>
1319
  );
1320
  }
frontend/src/pages/UploadPage/UploadPage.tsx CHANGED
@@ -616,7 +616,7 @@ export default function UploadPage() {
616
  },
617
  body: new URLSearchParams({
618
  title: title || 'Generated Caption',
619
- prompt: imageType === 'drone_image' ? 'DEFAULT_DRONE_IMAGE' : 'DEFAULT_CRISIS_MAP',
620
  ...(modelName && { model_name: modelName })
621
  })
622
  },
@@ -737,7 +737,7 @@ export default function UploadPage() {
737
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
738
  body: new URLSearchParams({
739
  title: 'Generated Caption',
740
- prompt: imageType === 'drone_image' ? 'DEFAULT_DRONE_IMAGE' : 'DEFAULT_CRISIS_MAP',
741
  ...(modelName && { model_name: modelName }),
742
  }),
743
  });
 
616
  },
617
  body: new URLSearchParams({
618
  title: title || 'Generated Caption',
619
+ // No prompt specified - backend will use active prompt for image type
620
  ...(modelName && { model_name: modelName })
621
  })
622
  },
 
737
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
738
  body: new URLSearchParams({
739
  title: 'Generated Caption',
740
+ // No prompt specified - backend will use active prompt for image type
741
  ...(modelName && { model_name: modelName }),
742
  }),
743
  });
py_backend/alembic/versions/0011_add_prompt_image_type_and_active.py ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Add image_type and is_active to prompts table
2
+
3
+ Revision ID: 0011
4
+ Revises: 0010
5
+ Create Date: 2024-01-01 00:00:00.000000
6
+
7
+ """
8
+ from alembic import op
9
+ import sqlalchemy as sa
10
+
11
+ # revision identifiers, used by Alembic.
12
+ revision = '0011'
13
+ down_revision = '0010'
14
+ branch_labels = None
15
+ depends_on = None
16
+
17
+ def upgrade():
18
+ # Add new columns to prompts table
19
+ op.add_column('prompts', sa.Column('image_type', sa.String(), nullable=True))
20
+ op.add_column('prompts', sa.Column('is_active', sa.Boolean(), server_default=sa.text('false'), nullable=False))
21
+
22
+ # Update existing prompts to set image_type and is_active
23
+ op.execute("""
24
+ UPDATE prompts
25
+ SET image_type = 'crisis_map', is_active = true
26
+ WHERE p_code = 'DEFAULT_CRISIS_MAP'
27
+ """)
28
+
29
+ op.execute("""
30
+ UPDATE prompts
31
+ SET image_type = 'drone_image', is_active = true
32
+ WHERE p_code = 'DEFAULT_DRONE_IMAGE'
33
+ """)
34
+
35
+ # Make image_type NOT NULL after setting values
36
+ op.alter_column('prompts', 'image_type', nullable=False)
37
+
38
+ # Create foreign key constraint
39
+ op.create_foreign_key(
40
+ 'fk_prompts_image_type',
41
+ 'prompts',
42
+ 'image_types',
43
+ ['image_type'],
44
+ ['image_type']
45
+ )
46
+
47
+ # Create index on image_type for performance
48
+ op.create_index('ix_prompts_image_type', 'prompts', ['image_type'])
49
+
50
+ def downgrade():
51
+ # Remove the index
52
+ op.drop_index('ix_prompts_image_type', 'prompts')
53
+
54
+ # Remove the foreign key constraint
55
+ op.drop_constraint('fk_prompts_image_type', 'prompts', type_='foreignkey')
56
+
57
+ # Remove the columns
58
+ op.drop_column('prompts', 'is_active')
59
+ op.drop_column('prompts', 'image_type')
py_backend/alembic/versions/0012_fix_prompt_constraints.py ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Fix prompt constraints to allow multiple inactive prompts per image type
2
+
3
+ Revision ID: 0012
4
+ Revises: 0011
5
+ Create Date: 2024-01-01 00:00:00.000000
6
+
7
+ """
8
+ from alembic import op
9
+ import sqlalchemy as sa
10
+
11
+ # revision identifiers, used by Alembic.
12
+ revision = '0012'
13
+ down_revision = '0011'
14
+ branch_labels = None
15
+ depends_on = None
16
+
17
+ def upgrade():
18
+ # Drop the incorrect unique constraint
19
+ op.drop_constraint('uq_prompts_image_type_active', 'prompts', type_='unique')
20
+
21
+ # Create a partial unique constraint that only applies when is_active = true
22
+ # This allows multiple inactive prompts per image type, but only one active
23
+ op.execute("""
24
+ CREATE UNIQUE INDEX uq_prompts_active_per_type
25
+ ON prompts (image_type)
26
+ WHERE is_active = true
27
+ """)
28
+
29
+ def downgrade():
30
+ # Drop the partial unique constraint
31
+ op.execute("DROP INDEX IF EXISTS uq_prompts_active_per_type")
32
+
33
+ # Recreate the original constraint
34
+ op.create_unique_constraint('uq_prompts_image_type_active', 'prompts', ['image_type', 'is_active'])
py_backend/app/crud.py CHANGED
@@ -142,13 +142,111 @@ def get_prompt_by_label(db: Session, label: str):
142
  """Get a specific prompt by label text"""
143
  return db.query(models.Prompts).filter(models.Prompts.label == label).first()
144
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
145
  def update_prompt(db: Session, p_code: str, prompt_update: schemas.PromptUpdate):
146
  """Update a specific prompt by code"""
147
  prompt = db.query(models.Prompts).filter(models.Prompts.p_code == p_code).first()
148
  if not prompt:
149
  return None
150
 
151
- for field, value in prompt_update.dict(exclude_unset=True).items():
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
152
  setattr(prompt, field, value)
153
 
154
  db.commit()
 
142
  """Get a specific prompt by label text"""
143
  return db.query(models.Prompts).filter(models.Prompts.label == label).first()
144
 
145
+ def get_active_prompt_by_image_type(db: Session, image_type: str):
146
+ """Get the active prompt for a specific image type"""
147
+ return db.query(models.Prompts).filter(
148
+ models.Prompts.image_type == image_type,
149
+ models.Prompts.is_active == True
150
+ ).first()
151
+
152
+ def toggle_prompt_active_status(db: Session, p_code: str, image_type: str):
153
+ """Toggle the active status of a prompt for a specific image type"""
154
+ # Validate that the image_type exists
155
+ image_type_obj = db.query(models.ImageTypes).filter(models.ImageTypes.image_type == image_type).first()
156
+ if not image_type_obj:
157
+ raise ValueError(f"Invalid image_type: {image_type}")
158
+
159
+ # Get the prompt to toggle
160
+ prompt = db.query(models.Prompts).filter(models.Prompts.p_code == p_code).first()
161
+ if not prompt:
162
+ return None
163
+
164
+ # If the prompt is already active, deactivate it
165
+ if prompt.is_active:
166
+ prompt.is_active = False
167
+ db.commit()
168
+ db.refresh(prompt)
169
+ return prompt
170
+
171
+ # If the prompt is not active, first deactivate the currently active prompt
172
+ # then activate this one
173
+ current_active = db.query(models.Prompts).filter(
174
+ models.Prompts.image_type == image_type,
175
+ models.Prompts.is_active == True
176
+ ).first()
177
+
178
+ if current_active:
179
+ current_active.is_active = False
180
+ # Commit the deactivation first to avoid constraint violation
181
+ db.commit()
182
+
183
+ prompt.is_active = True
184
+ db.commit()
185
+ db.refresh(prompt)
186
+ return prompt
187
+
188
+ def create_prompt(db: Session, prompt_data: schemas.PromptCreate):
189
+ """Create a new prompt"""
190
+ # Validate that the image_type exists
191
+ image_type_obj = db.query(models.ImageTypes).filter(models.ImageTypes.image_type == prompt_data.image_type).first()
192
+ if not image_type_obj:
193
+ raise ValueError(f"Invalid image_type: {prompt_data.image_type}")
194
+
195
+ # Check if prompt code already exists
196
+ existing_prompt = db.query(models.Prompts).filter(models.Prompts.p_code == prompt_data.p_code).first()
197
+ if existing_prompt:
198
+ raise ValueError(f"Prompt with code '{prompt_data.p_code}' already exists")
199
+
200
+ # If this prompt is set as active, deactivate the currently active prompt for this image type
201
+ if prompt_data.is_active:
202
+ current_active = db.query(models.Prompts).filter(
203
+ models.Prompts.image_type == prompt_data.image_type,
204
+ models.Prompts.is_active == True
205
+ ).first()
206
+
207
+ if current_active:
208
+ current_active.is_active = False
209
+ # Commit the deactivation first to avoid constraint violation
210
+ db.commit()
211
+
212
+ # Create the new prompt
213
+ new_prompt = models.Prompts(
214
+ p_code=prompt_data.p_code,
215
+ label=prompt_data.label,
216
+ metadata_instructions=prompt_data.metadata_instructions,
217
+ image_type=prompt_data.image_type,
218
+ is_active=prompt_data.is_active
219
+ )
220
+
221
+ db.add(new_prompt)
222
+ db.commit()
223
+ db.refresh(new_prompt)
224
+ return new_prompt
225
+
226
  def update_prompt(db: Session, p_code: str, prompt_update: schemas.PromptUpdate):
227
  """Update a specific prompt by code"""
228
  prompt = db.query(models.Prompts).filter(models.Prompts.p_code == p_code).first()
229
  if not prompt:
230
  return None
231
 
232
+ # Handle is_active field specially to maintain unique constraint
233
+ update_data = prompt_update.dict(exclude_unset=True)
234
+
235
+ # If we're setting this prompt as active, deactivate other prompts for this image type
236
+ if 'is_active' in update_data and update_data['is_active']:
237
+ current_active = db.query(models.Prompts).filter(
238
+ models.Prompts.image_type == prompt.image_type,
239
+ models.Prompts.is_active == True,
240
+ models.Prompts.p_code != p_code # Exclude current prompt
241
+ ).first()
242
+
243
+ if current_active:
244
+ current_active.is_active = False
245
+ # Commit the deactivation first to avoid constraint violation
246
+ db.commit()
247
+
248
+ # Update all fields
249
+ for field, value in update_data.items():
250
  setattr(prompt, field, value)
251
 
252
  db.commit()
py_backend/app/main.py CHANGED
@@ -19,7 +19,6 @@ from app.routers.schemas import router as schemas_router
19
 
20
  app = FastAPI(title="PromptAid Vision")
21
 
22
- # Add request logging middleware
23
  @app.middleware("http")
24
  async def log_requests(request, call_next):
25
  print(f"DEBUG: {request.method} {request.url.path}")
@@ -36,7 +35,6 @@ app.add_middleware(
36
  allow_headers=["*"],
37
  )
38
 
39
- # API routes - must come BEFORE static file mounting
40
  app.include_router(caption.router, prefix="/api", tags=["captions"])
41
  app.include_router(metadata.router, prefix="/api", tags=["metadata"])
42
  app.include_router(models.router, prefix="/api", tags=["models"])
@@ -58,17 +56,7 @@ async def list_images_no_slash():
58
  finally:
59
  db.close()
60
 
61
- @app.get("/api/prompts", include_in_schema=False)
62
- async def list_prompts_no_slash():
63
- """Handle /api/prompts without trailing slash to prevent 307 redirect"""
64
- from app.routers.prompts import get_prompts
65
- from app.database import SessionLocal
66
-
67
- db = SessionLocal()
68
- try:
69
- return get_prompts(db)
70
- finally:
71
- db.close()
72
 
73
  @app.get("/health", include_in_schema=False, response_class=JSONResponse)
74
  async def health():
@@ -82,17 +70,16 @@ def root():
82
  <p>OK</p>
83
  <p><a href="/app/">Open UI</a> • <a href="/docs">API Docs</a></p>"""
84
 
85
- # Static file serving - must come AFTER API routes
86
  if os.path.exists("/app"):
87
  STATIC_DIR = "/app/static"
88
  else:
89
- STATIC_DIR = "static" # Use relative path to py_backend/static
90
 
91
  print(f"Looking for static files in: {STATIC_DIR}")
92
 
93
  if os.path.isdir(STATIC_DIR):
94
  print(f"Static directory found: {STATIC_DIR}")
95
- # Mount static files at /app to avoid conflicts with /api routes
96
  app.mount("/app", StaticFiles(directory=STATIC_DIR, html=True), name="static")
97
  print(f"Static files mounted at /app from {STATIC_DIR}")
98
  else:
@@ -101,7 +88,6 @@ else:
101
  print(f"Parent directory contents: {os.listdir(os.path.dirname(os.path.dirname(__file__)))}")
102
  print(f"Attempting to find static directory...")
103
 
104
- # Try to find static directory
105
  possible_paths = [
106
  "static",
107
  "../static",
@@ -119,7 +105,7 @@ else:
119
  else:
120
  print("Could not find static directory - static file serving disabled")
121
 
122
- # SPA fallback - must come AFTER static file mounting
123
  @app.get("/app/{full_path:path}", include_in_schema=False)
124
  def spa_fallback(full_path: str):
125
  index = os.path.join(STATIC_DIR, "index.html")
@@ -127,7 +113,7 @@ def spa_fallback(full_path: str):
127
  return FileResponse(index)
128
  raise HTTPException(status_code=404, detail="Not Found")
129
 
130
- # Debug route to show all registered routes
131
  @app.get("/debug-routes", include_in_schema=False)
132
  async def debug_routes():
133
  """Show all registered routes for debugging"""
 
19
 
20
  app = FastAPI(title="PromptAid Vision")
21
 
 
22
  @app.middleware("http")
23
  async def log_requests(request, call_next):
24
  print(f"DEBUG: {request.method} {request.url.path}")
 
35
  allow_headers=["*"],
36
  )
37
 
 
38
  app.include_router(caption.router, prefix="/api", tags=["captions"])
39
  app.include_router(metadata.router, prefix="/api", tags=["metadata"])
40
  app.include_router(models.router, prefix="/api", tags=["models"])
 
56
  finally:
57
  db.close()
58
 
59
+
 
 
 
 
 
 
 
 
 
 
60
 
61
  @app.get("/health", include_in_schema=False, response_class=JSONResponse)
62
  async def health():
 
70
  <p>OK</p>
71
  <p><a href="/app/">Open UI</a> • <a href="/docs">API Docs</a></p>"""
72
 
 
73
  if os.path.exists("/app"):
74
  STATIC_DIR = "/app/static"
75
  else:
76
+ STATIC_DIR = "static"
77
 
78
  print(f"Looking for static files in: {STATIC_DIR}")
79
 
80
  if os.path.isdir(STATIC_DIR):
81
  print(f"Static directory found: {STATIC_DIR}")
82
+
83
  app.mount("/app", StaticFiles(directory=STATIC_DIR, html=True), name="static")
84
  print(f"Static files mounted at /app from {STATIC_DIR}")
85
  else:
 
88
  print(f"Parent directory contents: {os.listdir(os.path.dirname(os.path.dirname(__file__)))}")
89
  print(f"Attempting to find static directory...")
90
 
 
91
  possible_paths = [
92
  "static",
93
  "../static",
 
105
  else:
106
  print("Could not find static directory - static file serving disabled")
107
 
108
+
109
  @app.get("/app/{full_path:path}", include_in_schema=False)
110
  def spa_fallback(full_path: str):
111
  index = os.path.join(STATIC_DIR, "index.html")
 
113
  return FileResponse(index)
114
  raise HTTPException(status_code=404, detail="Not Found")
115
 
116
+
117
  @app.get("/debug-routes", include_in_schema=False)
118
  async def debug_routes():
119
  """Show all registered routes for debugging"""
py_backend/app/models.py CHANGED
@@ -62,6 +62,11 @@ class Prompts(Base):
62
  p_code = Column(String, primary_key=True)
63
  label = Column(Text, nullable=False)
64
  metadata_instructions = Column(Text, nullable=True)
 
 
 
 
 
65
 
66
  class Models(Base):
67
  __tablename__ = "models"
 
62
  p_code = Column(String, primary_key=True)
63
  label = Column(Text, nullable=False)
64
  metadata_instructions = Column(Text, nullable=True)
65
+ image_type = Column(String, ForeignKey("image_types.image_type"), nullable=False)
66
+ is_active = Column(Boolean, default=False, nullable=False)
67
+
68
+ # Relationship to image_types table
69
+ image_type_r = relationship("ImageTypes")
70
 
71
  class Models(Base):
72
  __tablename__ = "models"
py_backend/app/routers/caption.py CHANGED
@@ -87,7 +87,7 @@ def get_db():
87
  async def create_caption(
88
  image_id: str,
89
  title: str = Form(...),
90
- prompt: str = Form(...),
91
  model_name: str | None = Form(None),
92
  db: Session = Depends(get_db),
93
  ):
@@ -97,14 +97,22 @@ async def create_caption(
97
  if not img:
98
  raise HTTPException(404, "image not found")
99
 
100
-
101
- print(f"Looking for prompt: '{prompt}' (type: {type(prompt)})")
102
-
103
- prompt_obj = crud.get_prompt(db, prompt)
104
-
105
- if not prompt_obj:
106
- print(f"Prompt not found by code, trying to find by label...")
107
- prompt_obj = crud.get_prompt_by_label(db, prompt)
 
 
 
 
 
 
 
 
108
 
109
  print(f"Prompt lookup result: {prompt_obj}")
110
  if not prompt_obj:
 
87
  async def create_caption(
88
  image_id: str,
89
  title: str = Form(...),
90
+ prompt: str = Form(None), # Made optional since we'll use active prompts
91
  model_name: str | None = Form(None),
92
  db: Session = Depends(get_db),
93
  ):
 
97
  if not img:
98
  raise HTTPException(404, "image not found")
99
 
100
+ # Get the active prompt for this image type
101
+ if prompt:
102
+ # If prompt is provided, use it (for backward compatibility)
103
+ print(f"Looking for prompt: '{prompt}' (type: {type(prompt)})")
104
+ prompt_obj = crud.get_prompt(db, prompt)
105
+
106
+ if not prompt_obj:
107
+ print(f"Prompt not found by code, trying to find by label...")
108
+ prompt_obj = crud.get_prompt_by_label(db, prompt)
109
+ else:
110
+ # Use the active prompt for the image type
111
+ print(f"Looking for active prompt for image type: {img.image_type}")
112
+ prompt_obj = crud.get_active_prompt_by_image_type(db, img.image_type)
113
+
114
+ if not prompt_obj:
115
+ raise HTTPException(400, f"No active prompt found for image type '{img.image_type}'")
116
 
117
  print(f"Prompt lookup result: {prompt_obj}")
118
  if not prompt_obj:
py_backend/app/routers/prompts.py CHANGED
@@ -34,3 +34,35 @@ def update_prompt(p_code: str, prompt_update: schemas.PromptUpdate, db: Session
34
  from fastapi import HTTPException
35
  raise HTTPException(404, "Prompt not found")
36
  return prompt
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  from fastapi import HTTPException
35
  raise HTTPException(404, "Prompt not found")
36
  return prompt
37
+
38
+ @router.post("/{p_code}/toggle-active", response_model=schemas.PromptOut)
39
+ def toggle_prompt_active(p_code: str, image_type: str, db: Session = Depends(get_db)):
40
+ """Toggle the active status of a prompt for a specific image type"""
41
+ try:
42
+ prompt = crud.toggle_prompt_active_status(db, p_code, image_type)
43
+ if not prompt:
44
+ from fastapi import HTTPException
45
+ raise HTTPException(404, "Prompt not found")
46
+ return prompt
47
+ except ValueError as e:
48
+ from fastapi import HTTPException
49
+ raise HTTPException(400, str(e))
50
+
51
+ @router.get("/active/{image_type}", response_model=schemas.PromptOut)
52
+ def get_active_prompt(image_type: str, db: Session = Depends(get_db)):
53
+ """Get the active prompt for a specific image type"""
54
+ prompt = crud.get_active_prompt_by_image_type(db, image_type)
55
+ if not prompt:
56
+ from fastapi import HTTPException
57
+ raise HTTPException(404, "No active prompt found for this image type")
58
+ return prompt
59
+
60
+ @router.post("/", response_model=schemas.PromptOut)
61
+ def create_prompt(prompt_data: schemas.PromptCreate, db: Session = Depends(get_db)):
62
+ """Create a new prompt"""
63
+ try:
64
+ prompt = crud.create_prompt(db, prompt_data)
65
+ return prompt
66
+ except ValueError as e:
67
+ from fastapi import HTTPException
68
+ raise HTTPException(400, str(e))
py_backend/app/schemas.py CHANGED
@@ -98,13 +98,24 @@ class PromptOut(BaseModel):
98
  p_code: str
99
  label: str
100
  metadata_instructions: str | None = None
 
 
101
 
102
  class Config:
103
  from_attributes = True
104
 
 
 
 
 
 
 
 
105
  class PromptUpdate(BaseModel):
106
  label: str
107
  metadata_instructions: str | None = None
 
 
108
 
109
  class SourceOut(BaseModel):
110
  s_code: str
 
98
  p_code: str
99
  label: str
100
  metadata_instructions: str | None = None
101
+ image_type: str
102
+ is_active: bool
103
 
104
  class Config:
105
  from_attributes = True
106
 
107
+ class PromptCreate(BaseModel):
108
+ p_code: str
109
+ label: str
110
+ metadata_instructions: str | None = None
111
+ image_type: str
112
+ is_active: bool
113
+
114
  class PromptUpdate(BaseModel):
115
  label: str
116
  metadata_instructions: str | None = None
117
+ image_type: str
118
+ is_active: bool
119
 
120
  class SourceOut(BaseModel):
121
  s_code: str