spagestic commited on
Commit
a9deb6d
·
1 Parent(s): 0c56308

right panel now toggles between two full-height views

Browse files
Files changed (6) hide show
  1. assets/app.js +151 -56
  2. assets/globe.css +1 -2
  3. assets/index.html +20 -24
  4. assets/server.css +57 -34
  5. ui/intake/panel.py +6 -9
  6. ui/workspace.py +22 -25
assets/app.js CHANGED
@@ -2,6 +2,18 @@ import { Client } from "https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.m
2
 
3
  const client = await Client.connect(window.location.origin);
4
 
 
 
 
 
 
 
 
 
 
 
 
 
5
  function unwrapResult(result) {
6
  if (result && Array.isArray(result.data)) {
7
  return result.data.length === 1 ? result.data[0] : result.data;
@@ -14,41 +26,49 @@ const state = {
14
  globeState: { version: 0, markers: [], highlights: [], fly_to: null },
15
  choices: null,
16
  busy: false,
 
17
  };
18
 
19
  const els = {
 
 
 
20
  statusBanner: document.getElementById("status-banner"),
21
  chatMessages: document.getElementById("chat-messages"),
22
  chatInput: document.getElementById("chat-input"),
23
  chatSend: document.getElementById("chat-send"),
 
24
  createPrompt: document.getElementById("create-prompt"),
25
  personaList: document.getElementById("persona-list"),
26
- tabButtons: document.querySelectorAll(".tab-button"),
27
- formTab: document.getElementById("form-tab"),
28
- chatTab: document.getElementById("chat-tab"),
29
  };
30
 
31
- function showTab(name) {
32
- const isForm = name === "form";
33
- els.formTab.classList.toggle("active", isForm);
34
- els.chatTab.classList.toggle("active", !isForm);
35
- els.tabButtons.forEach((button) => {
36
- button.classList.toggle("active", button.dataset.tab === name);
37
- });
38
  }
39
 
40
- els.tabButtons.forEach((button) => {
41
- button.addEventListener("click", () => showTab(button.dataset.tab));
42
- });
43
-
44
  function setStatus(message) {
 
 
 
 
45
  if (!message) {
46
- els.statusBanner.classList.remove("visible");
47
- els.statusBanner.textContent = "";
 
 
48
  return;
49
  }
50
- els.statusBanner.textContent = message;
51
- els.statusBanner.classList.add("visible");
 
 
 
 
 
 
 
 
 
52
  }
53
 
54
  function setBusy(busy) {
@@ -84,6 +104,40 @@ function selectedValues(select) {
84
  return value || null;
85
  }
86
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
  function renderMessages() {
88
  els.chatMessages.innerHTML = "";
89
  for (const message of state.history) {
@@ -113,6 +167,20 @@ function applyGlobeState(globeState) {
113
  window.BorderlessGlobe?.applyState(globeState);
114
  }
115
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
116
  async function loadChoices() {
117
  const result = await client.predict("/get_intake_choices", {});
118
  const choices = unwrapResult(result);
@@ -137,16 +205,52 @@ async function loadChoices() {
137
  }
138
  }
139
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
140
  async function loadPersonaPrompt(personaId) {
 
 
 
 
 
141
  setBusy(true);
142
  setStatus("Building demo persona prompt...");
143
  try {
144
  const result = await client.predict("/build_persona_prompt", {
145
  persona_id: personaId,
146
  });
147
- els.chatInput.value = unwrapResult(result) || "";
148
- showTab("chat");
149
- setStatus("");
 
 
 
150
  } catch (error) {
151
  setStatus(`Could not load persona prompt: ${error.message || error}`);
152
  } finally {
@@ -154,26 +258,28 @@ async function loadPersonaPrompt(personaId) {
154
  }
155
  }
156
 
157
- async function createResearchPrompt() {
 
 
 
 
 
 
158
  setBusy(true);
159
  setStatus("Building research prompt...");
160
  try {
161
- const result = await client.predict("/build_research_prompt", {
162
- current_country: selectedValues(document.getElementById("current-country")),
163
- residence_status: selectedValues(document.getElementById("residence-status")),
164
- education: selectedValues(document.getElementById("education")),
165
- occupation: selectedValues(document.getElementById("occupation")),
166
- experience: selectedValues(document.getElementById("experience")),
167
- budget: selectedValues(document.getElementById("budget")),
168
- family: selectedValues(document.getElementById("family")),
169
- timeline: selectedValues(document.getElementById("timeline")),
170
- goals: document.getElementById("goals").value || "",
171
- });
172
- els.chatInput.value = unwrapResult(result) || "";
173
- showTab("chat");
174
  setStatus("");
175
  } catch (error) {
176
- setStatus(`Could not build prompt: ${error.message || error}`);
177
  } finally {
178
  setBusy(false);
179
  }
@@ -185,32 +291,21 @@ async function sendChat() {
185
  return;
186
  }
187
 
188
- setBusy(true);
189
- setStatus("Researching pathways...");
190
  els.chatInput.value = "";
191
-
192
  try {
193
- const result = await client.predict("/chat", {
194
- message,
195
- history: state.history,
196
- globe_state: state.globeState,
197
- });
198
- const payload = unwrapResult(result);
199
- state.history = payload.history || state.history;
200
- renderMessages();
201
- if (payload.globe_state) {
202
- applyGlobeState(payload.globe_state);
203
- }
204
- setStatus("");
205
- } catch (error) {
206
- setStatus(`Chat failed: ${error.message || error}`);
207
  els.chatInput.value = message;
208
- } finally {
209
- setBusy(false);
210
  }
211
  }
212
 
213
- els.createPrompt.addEventListener("click", createResearchPrompt);
 
 
 
 
 
 
214
  els.chatSend.addEventListener("click", sendChat);
215
  els.chatInput.addEventListener("keydown", (event) => {
216
  if (event.key === "Enter" && !event.shiftKey) {
 
2
 
3
  const client = await Client.connect(window.location.origin);
4
 
5
+ const REQUIRED_FIELDS = [
6
+ "current-country",
7
+ "residence-status",
8
+ "education",
9
+ "occupation",
10
+ "experience",
11
+ "budget",
12
+ "family",
13
+ "timeline",
14
+ "goals",
15
+ ];
16
+
17
  function unwrapResult(result) {
18
  if (result && Array.isArray(result.data)) {
19
  return result.data.length === 1 ? result.data[0] : result.data;
 
26
  globeState: { version: 0, markers: [], highlights: [], fly_to: null },
27
  choices: null,
28
  busy: false,
29
+ view: "form",
30
  };
31
 
32
  const els = {
33
+ formView: document.getElementById("form-view"),
34
+ chatView: document.getElementById("chat-view"),
35
+ formStatus: document.getElementById("form-status"),
36
  statusBanner: document.getElementById("status-banner"),
37
  chatMessages: document.getElementById("chat-messages"),
38
  chatInput: document.getElementById("chat-input"),
39
  chatSend: document.getElementById("chat-send"),
40
+ intakeForm: document.getElementById("intake-form"),
41
  createPrompt: document.getElementById("create-prompt"),
42
  personaList: document.getElementById("persona-list"),
 
 
 
43
  };
44
 
45
+ function activeStatusBanner() {
46
+ return state.view === "form" ? els.formStatus : els.statusBanner;
 
 
 
 
 
47
  }
48
 
 
 
 
 
49
  function setStatus(message) {
50
+ const banner = activeStatusBanner();
51
+ const inactive =
52
+ state.view === "form" ? els.statusBanner : els.formStatus;
53
+
54
  if (!message) {
55
+ banner.classList.remove("visible");
56
+ banner.textContent = "";
57
+ inactive.classList.remove("visible");
58
+ inactive.textContent = "";
59
  return;
60
  }
61
+ banner.textContent = message;
62
+ banner.classList.add("visible");
63
+ inactive.classList.remove("visible");
64
+ inactive.textContent = "";
65
+ }
66
+
67
+ function showChatView() {
68
+ state.view = "chat";
69
+ els.formView.classList.remove("is-active");
70
+ els.chatView.classList.add("is-active");
71
+ setStatus("");
72
  }
73
 
74
  function setBusy(busy) {
 
104
  return value || null;
105
  }
106
 
107
+ function fieldValue(id) {
108
+ const element = document.getElementById(id);
109
+ if (element.tagName === "TEXTAREA") {
110
+ return element.value.trim();
111
+ }
112
+ return selectedValues(element);
113
+ }
114
+
115
+ function setFieldInvalid(id, invalid) {
116
+ const element = document.getElementById(id);
117
+ element.classList.toggle("invalid", invalid);
118
+ }
119
+
120
+ function validateForm() {
121
+ let valid = true;
122
+ for (const id of REQUIRED_FIELDS) {
123
+ const empty = !fieldValue(id);
124
+ setFieldInvalid(id, empty);
125
+ if (empty) {
126
+ valid = false;
127
+ }
128
+ }
129
+ if (!valid) {
130
+ setStatus("Please fill in all required fields before submitting.");
131
+ }
132
+ return valid;
133
+ }
134
+
135
+ function clearFieldValidation() {
136
+ for (const id of REQUIRED_FIELDS) {
137
+ setFieldInvalid(id, false);
138
+ }
139
+ }
140
+
141
  function renderMessages() {
142
  els.chatMessages.innerHTML = "";
143
  for (const message of state.history) {
 
167
  window.BorderlessGlobe?.applyState(globeState);
168
  }
169
 
170
+ function formPayload() {
171
+ return {
172
+ current_country: selectedValues(document.getElementById("current-country")),
173
+ residence_status: selectedValues(document.getElementById("residence-status")),
174
+ education: selectedValues(document.getElementById("education")),
175
+ occupation: selectedValues(document.getElementById("occupation")),
176
+ experience: selectedValues(document.getElementById("experience")),
177
+ budget: selectedValues(document.getElementById("budget")),
178
+ family: selectedValues(document.getElementById("family")),
179
+ timeline: selectedValues(document.getElementById("timeline")),
180
+ goals: document.getElementById("goals").value.trim(),
181
+ };
182
+ }
183
+
184
  async function loadChoices() {
185
  const result = await client.predict("/get_intake_choices", {});
186
  const choices = unwrapResult(result);
 
205
  }
206
  }
207
 
208
+ async function runChat(message) {
209
+ const result = await client.predict("/chat", {
210
+ message,
211
+ history: state.history,
212
+ globe_state: state.globeState,
213
+ });
214
+ const payload = unwrapResult(result);
215
+ state.history = payload.history || state.history;
216
+ renderMessages();
217
+ if (payload.globe_state) {
218
+ applyGlobeState(payload.globe_state);
219
+ }
220
+ }
221
+
222
+ async function sendChatMessage(message) {
223
+ setBusy(true);
224
+ setStatus("Researching pathways...");
225
+ try {
226
+ await runChat(message);
227
+ setStatus("");
228
+ } catch (error) {
229
+ setStatus(`Chat failed: ${error.message || error}`);
230
+ throw error;
231
+ } finally {
232
+ setBusy(false);
233
+ }
234
+ }
235
+
236
  async function loadPersonaPrompt(personaId) {
237
+ if (state.busy) {
238
+ return;
239
+ }
240
+
241
+ showChatView();
242
  setBusy(true);
243
  setStatus("Building demo persona prompt...");
244
  try {
245
  const result = await client.predict("/build_persona_prompt", {
246
  persona_id: personaId,
247
  });
248
+ const message = unwrapResult(result) || "";
249
+ if (!message) {
250
+ setStatus("Could not load persona prompt.");
251
+ return;
252
+ }
253
+ await sendChatMessage(message);
254
  } catch (error) {
255
  setStatus(`Could not load persona prompt: ${error.message || error}`);
256
  } finally {
 
258
  }
259
  }
260
 
261
+ async function submitForm(event) {
262
+ event.preventDefault();
263
+ if (state.busy || !validateForm()) {
264
+ return;
265
+ }
266
+
267
+ showChatView();
268
  setBusy(true);
269
  setStatus("Building research prompt...");
270
  try {
271
+ const result = await client.predict("/build_research_prompt", formPayload());
272
+ const message = unwrapResult(result) || "";
273
+ if (!message) {
274
+ setStatus("Could not build prompt.");
275
+ return;
276
+ }
277
+ clearFieldValidation();
278
+ setStatus("Researching pathways...");
279
+ await runChat(message);
 
 
 
 
280
  setStatus("");
281
  } catch (error) {
282
+ setStatus(`Submission failed: ${error.message || error}`);
283
  } finally {
284
  setBusy(false);
285
  }
 
291
  return;
292
  }
293
 
 
 
294
  els.chatInput.value = "";
 
295
  try {
296
+ await sendChatMessage(message);
297
+ } catch {
 
 
 
 
 
 
 
 
 
 
 
 
298
  els.chatInput.value = message;
 
 
299
  }
300
  }
301
 
302
+ for (const id of REQUIRED_FIELDS) {
303
+ const element = document.getElementById(id);
304
+ element.addEventListener("input", () => setFieldInvalid(id, false));
305
+ element.addEventListener("change", () => setFieldInvalid(id, false));
306
+ }
307
+
308
+ els.intakeForm.addEventListener("submit", submitForm);
309
  els.chatSend.addEventListener("click", sendChat);
310
  els.chatInput.addEventListener("keydown", (event) => {
311
  if (event.key === "Enter" && !event.shiftKey) {
assets/globe.css CHANGED
@@ -13,8 +13,7 @@
13
  position: relative;
14
  width: 100%;
15
  height: 100%;
16
- min-height: 70vh;
17
- max-height: 80vh;
18
  border-radius: 12px;
19
  overflow: hidden;
20
  background:
 
13
  position: relative;
14
  width: 100%;
15
  height: 100%;
16
+ min-height: 0;
 
17
  border-radius: 12px;
18
  overflow: hidden;
19
  background:
assets/index.html CHANGED
@@ -28,8 +28,6 @@
28
 
29
  <main class="borderless-main-workspace">
30
  <section class="borderless-center-panel">
31
-
32
-
33
  <div class="globe-root globe-column">
34
  <div class="globe-shell is-loading">
35
  <div
@@ -45,75 +43,73 @@
45
  </section>
46
 
47
  <section class="borderless-chat-panel">
48
- <div class="tab-bar">
49
- <button type="button" class="tab-button active" data-tab="form">Form</button>
50
- <button type="button" class="tab-button" data-tab="chat">Chat</button>
51
- </div>
52
-
53
- <div id="form-tab" class="tab-panel active borderless-form-panel">
54
  <div class="form-row">
55
  <div class="form-field">
56
  <label for="current-country">Current country</label>
57
- <select id="current-country"></select>
58
  </div>
59
  <div class="form-field">
60
  <label for="residence-status">Current residence status</label>
61
- <select id="residence-status"></select>
62
  </div>
63
  </div>
64
  <div class="form-row">
65
  <div class="form-field">
66
  <label for="education">Education</label>
67
- <select id="education"></select>
68
  </div>
69
  <div class="form-field">
70
  <label for="occupation">Occupation</label>
71
- <select id="occupation"></select>
72
  </div>
73
  </div>
74
  <div class="form-row">
75
  <div class="form-field">
76
  <label for="experience">Experience</label>
77
- <select id="experience"></select>
78
  </div>
79
  <div class="form-field">
80
  <label for="timeline">Target timeline</label>
81
- <select id="timeline"></select>
82
  </div>
83
  </div>
84
  <div class="form-row">
85
  <div class="form-field">
86
  <label for="budget">Budget</label>
87
- <select id="budget"></select>
88
  </div>
89
  <div class="form-field">
90
  <label for="family">Family / dependents</label>
91
- <select id="family"></select>
92
  </div>
93
  </div>
94
- <div class="form-field">
95
  <label for="goals">Goals in everyday language</label>
96
  <textarea
97
  id="goals"
 
98
  placeholder="e.g. I want a realistic skilled-worker route, lower risk, and a path to permanent residence."
99
  ></textarea>
100
  </div>
101
- <div>
102
- <div class="borderless-examples-title">Demo personas</div>
103
  <div id="persona-list" class="persona-list"></div>
 
 
 
104
  </div>
105
- <button type="button" id="create-prompt" class="primary">
106
- Create research prompt
107
- </button>
108
  </div>
109
 
110
- <div id="chat-tab" class="tab-panel chat-panel">
111
  <div id="status-banner" class="status-banner"></div>
112
  <div id="chat-messages" class="chat-messages"></div>
113
  <div class="chat-input-row">
114
  <textarea
115
  id="chat-input"
116
- placeholder="Create a prompt from the form, choose a demo, or type directly..."
117
  ></textarea>
118
  <button type="button" id="chat-send" class="primary">Send</button>
119
  </div>
 
28
 
29
  <main class="borderless-main-workspace">
30
  <section class="borderless-center-panel">
 
 
31
  <div class="globe-root globe-column">
32
  <div class="globe-shell is-loading">
33
  <div
 
43
  </section>
44
 
45
  <section class="borderless-chat-panel">
46
+ <div id="form-view" class="side-view is-active">
47
+ <form id="intake-form" class="borderless-form-panel" novalidate>
 
 
 
 
48
  <div class="form-row">
49
  <div class="form-field">
50
  <label for="current-country">Current country</label>
51
+ <select id="current-country" required></select>
52
  </div>
53
  <div class="form-field">
54
  <label for="residence-status">Current residence status</label>
55
+ <select id="residence-status" required></select>
56
  </div>
57
  </div>
58
  <div class="form-row">
59
  <div class="form-field">
60
  <label for="education">Education</label>
61
+ <select id="education" required></select>
62
  </div>
63
  <div class="form-field">
64
  <label for="occupation">Occupation</label>
65
+ <select id="occupation" required></select>
66
  </div>
67
  </div>
68
  <div class="form-row">
69
  <div class="form-field">
70
  <label for="experience">Experience</label>
71
+ <select id="experience" required></select>
72
  </div>
73
  <div class="form-field">
74
  <label for="timeline">Target timeline</label>
75
+ <select id="timeline" required></select>
76
  </div>
77
  </div>
78
  <div class="form-row">
79
  <div class="form-field">
80
  <label for="budget">Budget</label>
81
+ <select id="budget" required></select>
82
  </div>
83
  <div class="form-field">
84
  <label for="family">Family / dependents</label>
85
+ <select id="family" required></select>
86
  </div>
87
  </div>
88
+ <div class="form-field form-goals-field">
89
  <label for="goals">Goals in everyday language</label>
90
  <textarea
91
  id="goals"
92
+ required
93
  placeholder="e.g. I want a realistic skilled-worker route, lower risk, and a path to permanent residence."
94
  ></textarea>
95
  </div>
96
+ <div class="form-footer">
 
97
  <div id="persona-list" class="persona-list"></div>
98
+ <button type="submit" id="create-prompt" class="primary">
99
+ Submit
100
+ </button>
101
  </div>
102
+ </form>
103
+ <div id="form-status" class="status-banner"></div>
 
104
  </div>
105
 
106
+ <div id="chat-view" class="side-view">
107
  <div id="status-banner" class="status-banner"></div>
108
  <div id="chat-messages" class="chat-messages"></div>
109
  <div class="chat-input-row">
110
  <textarea
111
  id="chat-input"
112
+ placeholder="Ask a follow-up question..."
113
  ></textarea>
114
  <button type="button" id="chat-send" class="primary">Send</button>
115
  </div>
assets/server.css CHANGED
@@ -21,7 +21,9 @@ a {
21
  .app-shell {
22
  display: flex;
23
  flex-direction: column;
 
24
  min-height: 100vh;
 
25
  }
26
 
27
  .app-header {
@@ -78,7 +80,8 @@ button:disabled {
78
 
79
  .borderless-center-panel,
80
  .borderless-chat-panel {
81
- min-height: 70vh;
 
82
  display: flex;
83
  flex-direction: column;
84
  gap: 8px;
@@ -102,39 +105,27 @@ button:disabled {
102
  .borderless-chat-panel {
103
  border-left: 1px solid rgba(148, 163, 184, 0.18);
104
  padding-left: 10px;
 
105
  }
106
 
107
- .tab-bar {
108
- display: flex;
109
- gap: 8px;
110
- margin-bottom: 8px;
111
- }
112
-
113
- .tab-bar button {
114
- flex: 1;
115
- }
116
-
117
- .tab-bar button.active {
118
- border-color: #f59e0b;
119
- color: #fbbf24;
120
- }
121
-
122
- .tab-panel {
123
  display: none;
124
  flex: 1 1 auto;
125
  min-height: 0;
126
- overflow: hidden;
 
127
  }
128
 
129
- .tab-panel.active {
130
  display: flex;
131
- flex-direction: column;
132
  }
133
 
134
  .borderless-form-panel {
135
  display: flex;
136
  flex-direction: column;
137
- gap: 8px;
 
 
138
  overflow-y: auto;
139
  padding-right: 4px;
140
  }
@@ -142,13 +133,31 @@ button:disabled {
142
  .form-row {
143
  display: grid;
144
  grid-template-columns: 1fr 1fr;
145
- gap: 8px;
146
  }
147
 
148
  .form-field {
149
  display: flex;
150
  flex-direction: column;
151
- gap: 4px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
152
  }
153
 
154
  .form-field label {
@@ -168,9 +177,15 @@ button:disabled {
168
  font: inherit;
169
  }
170
 
171
- .form-field textarea {
172
- min-height: 72px;
173
- resize: vertical;
 
 
 
 
 
 
174
  }
175
 
176
  .persona-list {
@@ -183,14 +198,6 @@ button:disabled {
183
  font-size: 0.85rem;
184
  }
185
 
186
- .chat-panel {
187
- display: flex;
188
- flex-direction: column;
189
- gap: 8px;
190
- flex: 1 1 auto;
191
- min-height: 0;
192
- }
193
-
194
  .chat-messages {
195
  flex: 1 1 auto;
196
  min-height: 0;
@@ -267,10 +274,21 @@ button:disabled {
267
  }
268
 
269
  @media (max-width: 900px) {
 
 
 
 
 
270
  .borderless-main-workspace {
271
  grid-template-columns: 1fr;
272
  }
273
 
 
 
 
 
 
 
274
  .borderless-chat-panel {
275
  border-left: 0;
276
  padding-left: 0;
@@ -279,4 +297,9 @@ button:disabled {
279
  .form-row {
280
  grid-template-columns: 1fr;
281
  }
 
 
 
 
 
282
  }
 
21
  .app-shell {
22
  display: flex;
23
  flex-direction: column;
24
+ height: 100vh;
25
  min-height: 100vh;
26
+ overflow: hidden;
27
  }
28
 
29
  .app-header {
 
80
 
81
  .borderless-center-panel,
82
  .borderless-chat-panel {
83
+ min-height: 0;
84
+ height: 100%;
85
  display: flex;
86
  flex-direction: column;
87
  gap: 8px;
 
105
  .borderless-chat-panel {
106
  border-left: 1px solid rgba(148, 163, 184, 0.18);
107
  padding-left: 10px;
108
+ overflow: hidden;
109
  }
110
 
111
+ .side-view {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
112
  display: none;
113
  flex: 1 1 auto;
114
  min-height: 0;
115
+ flex-direction: column;
116
+ gap: 8px;
117
  }
118
 
119
+ .side-view.is-active {
120
  display: flex;
 
121
  }
122
 
123
  .borderless-form-panel {
124
  display: flex;
125
  flex-direction: column;
126
+ gap: 14px;
127
+ flex: 1 1 auto;
128
+ min-height: 0;
129
  overflow-y: auto;
130
  padding-right: 4px;
131
  }
 
133
  .form-row {
134
  display: grid;
135
  grid-template-columns: 1fr 1fr;
136
+ gap: 12px;
137
  }
138
 
139
  .form-field {
140
  display: flex;
141
  flex-direction: column;
142
+ gap: 6px;
143
+ }
144
+
145
+ .form-goals-field {
146
+ flex: 1 1 auto;
147
+ min-height: 0;
148
+ }
149
+
150
+ .form-footer {
151
+ flex: 0 0 auto;
152
+ display: flex;
153
+ flex-direction: column;
154
+ gap: 12px;
155
+ margin-top: auto;
156
+ padding-top: 4px;
157
+ }
158
+
159
+ .form-footer .primary {
160
+ width: 100%;
161
  }
162
 
163
  .form-field label {
 
177
  font: inherit;
178
  }
179
 
180
+ .form-goals-field textarea {
181
+ flex: 1 1 auto;
182
+ min-height: 96px;
183
+ resize: none;
184
+ }
185
+
186
+ .form-field select.invalid,
187
+ .form-field textarea.invalid {
188
+ border-color: #f87171;
189
  }
190
 
191
  .persona-list {
 
198
  font-size: 0.85rem;
199
  }
200
 
 
 
 
 
 
 
 
 
201
  .chat-messages {
202
  flex: 1 1 auto;
203
  min-height: 0;
 
274
  }
275
 
276
  @media (max-width: 900px) {
277
+ .app-shell {
278
+ height: auto;
279
+ overflow: auto;
280
+ }
281
+
282
  .borderless-main-workspace {
283
  grid-template-columns: 1fr;
284
  }
285
 
286
+ .borderless-center-panel,
287
+ .borderless-chat-panel {
288
+ height: auto;
289
+ min-height: 50vh;
290
+ }
291
+
292
  .borderless-chat-panel {
293
  border-left: 0;
294
  padding-left: 0;
 
297
  .form-row {
298
  grid-template-columns: 1fr;
299
  }
300
+
301
+ .form-goals-field textarea {
302
+ min-height: 120px;
303
+ resize: vertical;
304
+ }
305
  }
ui/intake/panel.py CHANGED
@@ -33,7 +33,7 @@ def _searchable_dropdown(
33
  )
34
 
35
 
36
- def _create_research_prompt_and_open_chat(
37
  current_country,
38
  residence_status,
39
  education,
@@ -56,13 +56,10 @@ def _create_research_prompt_and_open_chat(
56
  family,
57
  timeline,
58
  goals,
59
- ), gr.update(selected="chat")
60
 
61
 
62
- def render_intake_panel(
63
- chat_input: gr.MultimodalTextbox,
64
- workspace_tabs: gr.Tabs,
65
- ) -> None:
66
  countries = country_choices()
67
 
68
  with gr.Column(elem_classes=["borderless-form-panel"]):
@@ -110,8 +107,8 @@ def render_intake_panel(
110
  "and a path to permanent residence."
111
  ),
112
  )
113
- gr.Button("Create research prompt", variant="primary").click(
114
- fn=_create_research_prompt_and_open_chat,
115
  inputs=[
116
  current_country,
117
  residence_status,
@@ -123,6 +120,6 @@ def render_intake_panel(
123
  timeline,
124
  goals,
125
  ],
126
- outputs=[chat_input, workspace_tabs],
127
  queue=False,
128
  )
 
33
  )
34
 
35
 
36
+ def _create_research_prompt(
37
  current_country,
38
  residence_status,
39
  education,
 
56
  family,
57
  timeline,
58
  goals,
59
+ )
60
 
61
 
62
+ def render_intake_panel(chat_input: gr.MultimodalTextbox) -> None:
 
 
 
63
  countries = country_choices()
64
 
65
  with gr.Column(elem_classes=["borderless-form-panel"]):
 
107
  "and a path to permanent residence."
108
  ),
109
  )
110
+ gr.Button("Submit", variant="primary").click(
111
+ fn=_create_research_prompt,
112
  inputs=[
113
  current_country,
114
  residence_status,
 
120
  timeline,
121
  goals,
122
  ],
123
+ outputs=[chat_input],
124
  queue=False,
125
  )
ui/workspace.py CHANGED
@@ -14,7 +14,7 @@ def create_main_workspace(
14
  history_container: gr.Column | None = None,
15
  ) -> gr.ChatInterface:
16
  """
17
- Compose the main workspace: globe panel, intake form tab, and chat tab.
18
  """
19
  globe_state = gr.JSON(value=empty_globe_state(), visible=False, elem_id="globe-state")
20
  chat_input = gr.MultimodalTextbox(
@@ -43,30 +43,27 @@ def create_main_workspace(
43
  min_width=360,
44
  elem_classes=["borderless-chat-panel"],
45
  ):
46
- with gr.Tabs(elem_classes=["borderless-tabs"]) as workspace_tabs:
47
- with gr.Tab("Form", elem_classes=["borderless-form-tab"]):
48
- render_intake_panel(chat_input, workspace_tabs)
49
- with gr.Tab("Chat", id="chat", elem_classes=["borderless-chat-tab"]):
50
- chat = SidebarChatInterface(
51
- respond,
52
- multimodal=True,
53
- chatbot=gr.Chatbot(
54
- show_label=False,
55
- container=False,
56
- elem_classes=["borderless-chatbot"],
57
- ),
58
- textbox=chat_input,
59
- history_container=history_container,
60
- fill_height=True,
61
- additional_inputs=[
62
- gr.State(BORDERLESS_SYSTEM_PROMPT),
63
- gr.State(DEFAULT_MAX_TOKENS),
64
- gr.State(DEFAULT_TEMPERATURE),
65
- gr.State(DEFAULT_TOP_P),
66
- globe_state,
67
- ],
68
- additional_outputs=[globe_state],
69
- )
70
 
71
  globe_state.change(
72
  fn=None,
 
14
  history_container: gr.Column | None = None,
15
  ) -> gr.ChatInterface:
16
  """
17
+ Compose the main workspace: globe panel, intake form, and chat.
18
  """
19
  globe_state = gr.JSON(value=empty_globe_state(), visible=False, elem_id="globe-state")
20
  chat_input = gr.MultimodalTextbox(
 
43
  min_width=360,
44
  elem_classes=["borderless-chat-panel"],
45
  ):
46
+ render_intake_panel(chat_input)
47
+ chat = SidebarChatInterface(
48
+ respond,
49
+ multimodal=True,
50
+ chatbot=gr.Chatbot(
51
+ show_label=False,
52
+ container=False,
53
+ elem_classes=["borderless-chatbot"],
54
+ ),
55
+ textbox=chat_input,
56
+ history_container=history_container,
57
+ fill_height=True,
58
+ additional_inputs=[
59
+ gr.State(BORDERLESS_SYSTEM_PROMPT),
60
+ gr.State(DEFAULT_MAX_TOKENS),
61
+ gr.State(DEFAULT_TEMPERATURE),
62
+ gr.State(DEFAULT_TOP_P),
63
+ globe_state,
64
+ ],
65
+ additional_outputs=[globe_state],
66
+ )
 
 
 
67
 
68
  globe_state.change(
69
  fn=None,