KartikJoshiUK commited on
Commit
cb893b8
Β·
1 Parent(s): 314cc33

Improved UI

Browse files
Files changed (1) hide show
  1. app.py +672 -146
app.py CHANGED
@@ -12,10 +12,9 @@ session_ok = False
12
  tools_ok = False
13
  initialized = False
14
 
15
- pending_requests = [] # stores pending tool requests for UI
16
- pending_auth_token = "" # stores auth token for pending approvals
17
 
18
- # Demo mode - pre-fills fields for quick testing
19
  DEMO_MODE = True
20
 
21
  DEMO_SYSTEM_PROMPT = """You are a helpful AI assistant for a banking and task management API.
@@ -26,87 +25,463 @@ DEMO_ENV_VARS = """{
26
  "API_BASE_URL": "http://localhost:8000"
27
  }"""
28
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
  def start_session():
31
  global session_ok
32
  try:
33
  r = client.get(f"{API_URL}/session")
34
  if r.status_code == 200:
35
  session_ok = True
36
- return "βœ… **Session started!** You can now upload your Postman collection."
37
- return f"❌ **Failed to start session:** {r.text}\n\n*Make sure the backend server is running on {API_URL}*"
38
  except Exception as e:
39
- return f"❌ **Connection Error:** Could not connect to backend server.\n\n**Fix:** Run `cd demo/server && npm start` first.\n\n**Details:** {str(e)}"
40
 
41
 
42
  def upload_tools(file):
43
  global tools_ok
44
  if not session_ok:
45
- return "⚠️ **Please start the session first** by clicking the 'Start Session' button above."
 
46
  try:
47
- files = {"api": open(file, "rb")}
48
- r = client.post(f"{API_URL}/tools", files=files)
49
  if r.status_code == 200:
50
  tools_ok = True
51
- try:
52
- data = r.json()
53
- tool_count = data.get("toolCount", "multiple")
54
- return f"βœ… **Success!** Generated **{tool_count} tools** from your API collection.\n\n➑️ Now you can initialize the agent below."
55
- except:
56
- return "βœ… **Tools uploaded & generated successfully!**\n\n➑️ Now you can initialize the agent below."
57
- return f"❌ **Upload failed:** {r.text}"
58
  except Exception as e:
59
- return f"❌ **Error:** {str(e)}"
60
 
61
 
62
- def initialize(system_prompt, env_json):
63
  global initialized
64
- if not (session_ok and tools_ok):
65
- return "⚠️ **Please upload your Postman collection first!**"
66
 
67
  try:
68
- parsed_env = json.loads(env_json) if env_json.strip() else {}
69
  except Exception as e:
70
- return f"❌ **Invalid JSON in Environment Variables:**\n```\n{str(e)}\n```\n\nExpected format: `{{\"KEY\": \"value\"}}`"
 
 
 
71
 
72
  body = {
73
  "systemIntructions": system_prompt,
74
- "envVariables": parsed_env
 
 
 
75
  }
 
76
  try:
77
  r = client.post(f"{API_URL}/initialize", json=body)
78
  if r.status_code == 200:
79
  initialized = True
80
- return "βœ… **Agent initialized successfully!**\n\nπŸŽ‰ You can now start chatting below!"
81
- return f"❌ **Initialization failed:** {r.text}"
82
  except Exception as e:
83
- return f"❌ **Error:** {str(e)}"
84
-
85
-
86
- def detect_chart_url(text):
87
- quickchart_pattern = r'(https://quickchart\.io/chart\?[^\s\)]+)'
88
- match = re.search(quickchart_pattern, text)
89
- return match.group(1) if match else None
90
-
91
 
92
- def format_response_with_chart(message):
93
- chart_url = detect_chart_url(message)
94
- if chart_url:
95
- return message + f"\n\n![Chart Visualization]({chart_url})", chart_url
96
- return message, None
97
-
98
-
99
- def call_query(message, auth_token):
100
- headers = {"Authorization": f"Bearer {auth_token}"} if auth_token else {}
101
- params = {"query": message}
102
 
 
 
 
103
  try:
104
- # FIXED: endpoint must be /query
105
- r = client.get(f"{API_URL}", params=params, headers=headers, timeout=30)
106
  body = r.json()
107
  return {"message": body.get("message", ""), "pending": body.get("data", [])}
108
- except requests.exceptions.Timeout:
109
- return {"message": "⏱️ Request timed out. The API might be slow or unresponsive.", "pending": []}
110
  except Exception as e:
111
  return {"message": f"❌ Error: {str(e)}", "pending": []}
112
 
@@ -133,6 +508,12 @@ def chat_send(message, history, auth_token):
133
  # remove loading message safely
134
  history[-1] = (message, None)
135
 
 
 
 
 
 
 
136
  if result["pending"]:
137
  pending_requests = result["pending"]
138
  pending_auth_token = auth_token
@@ -149,57 +530,130 @@ def chat_send(message, history, auth_token):
149
 
150
  yield history, "", gr.update(visible=True), gr.update(visible=True), gr.update(visible=True)
151
  else:
152
- formatted_message, chart_url = format_response_with_chart(result["message"])
153
- # FIXED: assistant reply
154
- history[-1] = (message, formatted_message)
155
 
156
  yield history, "", gr.update(visible=False), gr.update(visible=False), gr.update(visible=False)
157
 
158
 
159
  def send_approval(approved, history):
160
  global pending_requests, pending_auth_token
161
-
162
  if not pending_requests:
163
- return "⚠️ No pending approvals", history, gr.update(visible=False), gr.update(visible=False), gr.update(visible=False)
 
164
 
165
- headers = {"Authorization": f"Bearer {pending_auth_token}"} if pending_auth_token else {}
166
- approval_data = [{"toolCallId": p["id"], "approved": approved} for p in pending_requests]
167
-
168
- try:
169
- r = client.post(f"{API_URL}/approval", json=approval_data, headers=headers)
170
- body = r.json()
171
- message = body.get("message", "Done")
172
- pending_requests = []
173
-
174
- # FIXED: approval result message
175
- history.append((None, message))
176
-
177
- return message, history, gr.update(visible=False), gr.update(visible=False), gr.update(visible=False)
178
- except Exception as e:
179
- err = f"❌ Error: {str(e)}"
180
- history.append((None, err))
181
- return err, history, gr.update(visible=False), gr.update(visible=False), gr.update(visible=False)
182
 
183
 
184
- def reset_chat():
185
- global session_ok, tools_ok, initialized, pending_requests, pending_auth_token
 
186
  try:
187
- client.delete(API_URL)
188
- except:
 
 
189
  pass
190
- session_ok = tools_ok = initialized = False
191
  pending_requests = []
192
  pending_auth_token = ""
193
  return [], gr.update(visible=False), gr.update(visible=False), gr.update(visible=False)
194
 
 
 
 
 
 
 
195
 
196
- # --- UI (unchanged below) ----------------------------------------------------
197
- with gr.Blocks(title="FluidTools - AI-Powered API Agent") as demo:
 
198
 
 
199
  gr.HTML("""
200
- <div style="text-align: center; padding: 30px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border-radius: 10px; margin-bottom: 30px;">
201
- <h1 style="margin: 0; font-size: 2.5em;">πŸ€– FluidTools</h1>
202
- <p style="margin: 10px 0 0 0; font-size: 1.2em;">Turn any Postman API collection into an AI-powered chatbot</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
203
  </div>
204
  """)
205
 
@@ -208,85 +662,157 @@ with gr.Blocks(title="FluidTools - AI-Powered API Agent") as demo:
208
  1. **Upload** your Postman collection (JSON export from Postman)
209
  2. **Initialize** the AI agent with your API tools
210
  3. **Chat** with your APIs using natural language - no coding required!
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
211
 
212
- ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
213
  """)
214
 
215
- with gr.Accordion("πŸš€ STEP 1 β€” Initialize System", open=True):
216
- step1_status = gr.Markdown("""
217
- **Current Status:** πŸ”΄ Not started
218
-
219
- **Instructions:**
220
- 1. Click **"Start Session"** to begin
221
- 2. Upload your Postman collection JSON file
222
- 3. (Optional) Customize the system prompt
223
- 4. (Optional) Add API keys/environment variables
224
- 5. Click **"Initialize Agent"** to start
225
- """)
226
-
227
- with gr.Row():
228
- start_btn = gr.Button("πŸ§ͺ Start Session", variant="primary", size="lg")
229
-
230
- gr.Markdown("---")
231
- gr.Markdown("**πŸ“ Upload Your Postman Collection**")
232
- gr.Markdown("*Export your collection from Postman as JSON (Collection v2.1)*")
233
-
234
- tool_file = gr.File(file_types=[".json"], label="Postman Collection File", file_count="single")
235
-
236
- gr.Markdown("---")
237
- gr.Markdown("**βš™οΈ Configure Agent (Optional)**")
238
-
239
- sys_prompt = gr.Textbox(
240
- label="System Prompt - Describe how the AI should behave",
241
- value=DEMO_SYSTEM_PROMPT if DEMO_MODE else "",
242
- lines=4
243
- )
244
-
245
- env_vars = gr.Textbox(
246
- label="Environment Variables (JSON format)",
247
- value=DEMO_ENV_VARS if DEMO_MODE else "",
248
- lines=4
249
- )
250
-
251
- with gr.Row():
252
- init_btn = gr.Button("πŸš€ Initialize Agent", variant="primary", interactive=False, size="lg")
253
-
254
- gr.Markdown("---")
255
- with gr.Accordion("πŸ’¬ STEP 2 β€” Chat with Your API", open=True):
256
-
257
- auth_box = gr.Textbox(label="πŸ” Authentication Token (optional)", type="password")
258
- chat = gr.Chatbot(height=500, avatar_images=(None, "πŸ€–"))
259
-
260
- msg = gr.Textbox(label="Your Message", lines=2, max_lines=5)
261
 
262
- with gr.Row():
263
- send = gr.Button("πŸ“€ Send", variant="primary")
264
- reset = gr.Button("πŸ”„ Reset Conversation")
265
 
266
- approval_section = gr.Markdown("### πŸ” Pending Approvals", visible=False)
267
- with gr.Row():
268
- approve_btn = gr.Button("βœ… Approve All", visible=False)
269
- reject_btn = gr.Button("❌ Reject All", visible=False)
270
 
271
- approval_result = gr.Textbox(label="Approval Result", visible=False, interactive=False)
 
 
 
 
 
272
 
273
- def update_buttons():
274
  return gr.update(interactive=session_ok), gr.update(interactive=(session_ok and tools_ok))
275
 
276
- start_btn.click(start_session, None, step1_status).then(update_buttons, None, [tool_file, init_btn])
277
- tool_file.upload(upload_tools, tool_file, step1_status).then(update_buttons, None, [tool_file, init_btn])
278
- init_btn.click(initialize, [sys_prompt, env_vars], step1_status)
279
 
280
- send.click(chat_send, [msg, chat, auth_box], [chat, msg, approval_section, approve_btn, reject_btn])
281
- msg.submit(chat_send, [msg, chat, auth_box], [chat, msg, approval_section, approve_btn, reject_btn])
282
 
283
- approve_btn.click(send_approval, [gr.State(True), chat],
284
- [approval_result, chat, approval_section, approve_btn, reject_btn])
285
 
286
- reject_btn.click(send_approval, [gr.State(False), chat],
287
- [approval_result, chat, approval_section, approve_btn, reject_btn])
288
 
289
- reset.click(reset_chat, None, [chat, approval_section, approve_btn, reject_btn])
290
 
291
  if __name__ == "__main__":
292
- demo.launch(server_port=7860, share=False, show_error=True)
 
12
  tools_ok = False
13
  initialized = False
14
 
15
+ pending_requests = []
16
+ pending_auth_token = ""
17
 
 
18
  DEMO_MODE = True
19
 
20
  DEMO_SYSTEM_PROMPT = """You are a helpful AI assistant for a banking and task management API.
 
25
  "API_BASE_URL": "http://localhost:8000"
26
  }"""
27
 
28
+ PROVIDERS = {
29
+ "nebius-free": {"name": "πŸ†“ Nebius Free (10 requests/day)", "requires_key": False},
30
+ "openai": {"name": "πŸ’° OpenAI (Your API Key)", "requires_key": True},
31
+ "anthropic": {"name": "πŸ’° Anthropic (Your API Key)", "requires_key": True},
32
+ "gemini": {"name": "πŸ’° Google Gemini (Your API Key)", "requires_key": True}
33
+ }
34
+ MODEL_MAP = {
35
+ "nebius-free": ["moonshotai/Kimi-K2-Instruct"],
36
+ "openai": ["gpt-4.1-mini", "gpt-4.1", "o1"],
37
+ "anthropic": ["claude-3-opus", "claude-3-sonnet", "claude-3-haiku"],
38
+ "gemini": ["gemini-2.5-flash-lite", "gemini-1.5-flash", "gemini-1.5-pro", "gemini-2.0"],
39
+ }
40
+
41
+
42
+ # ============================================
43
+ # GLASSMORPHISM CSS
44
+ # ============================================
45
+ CUSTOM_CSS = """
46
+ /* =========================================================
47
+ DARK GLASSMORPHISM THEME FOR FLUIDTOOLS
48
+ ========================================================= */
49
+ :root {
50
+ --glass-primary: rgba(102, 126, 234, 0.55);
51
+ --glass-primary-dark: rgba(118, 75, 162, 0.65);
52
+ --glass-accent: rgba(139, 92, 246, 0.65);
53
+ --glass-bg: rgba(18, 18, 26, 0.45);
54
+ --glass-hover: rgba(28, 28, 38, 0.57);
55
+ --glass-border: rgba(195, 170, 255, 0.38);
56
+ --text-light: rgba(235, 235, 255, 0.92);
57
+ --text-dim: rgba(190, 190, 220, 0.65);
58
+
59
+ --shadow-sm: 0 4px 6px rgba(0, 0, 0, 0.4);
60
+ --shadow-md: 0 10px 18px rgba(0, 0, 0, 0.45);
61
+ --shadow-lg: 0 18px 36px rgba(0, 0, 0, 0.55);
62
+ }
63
+
64
+ /* =========================================================
65
+ GLOBAL BACKGROUND (dark glass, animated)
66
+ ========================================================= */
67
+ .gradio-container {
68
+ background: linear-gradient(135deg,
69
+ #131421 0%,
70
+ #19172c 25%,
71
+ #201a3a 50%,
72
+ #161324 75%,
73
+ #131421 100%) !important;
74
+ background-size: 350% 350% !important;
75
+ animation: gradientShift 18s ease infinite;
76
+ min-height: 100vh;
77
+ font-family: "Inter", "SF Pro Display", "Segoe UI", sans-serif !important;
78
+ }
79
+ @keyframes gradientShift {
80
+ 0% { background-position: 0% 50%; }
81
+ 50% { background-position: 100% 50%; }
82
+ 100% { background-position: 0% 50%; }
83
+ }
84
+
85
+ /* =========================================================
86
+ UNIVERSAL TEXT VISIBILITY FOR DARK MODE
87
+ ========================================================= */
88
+ *, label, span, p, h1, h2, h3, div, .markdown, .prose {
89
+ color: var(--text-light) !important;
90
+ }
91
+
92
+ /* =========================================================
93
+ GLASS CARDS
94
+ ========================================================= */
95
+ .glass-card {
96
+ background: var(--glass-bg) !important;
97
+ border: 1px solid var(--glass-border) !important;
98
+ border-radius: 20px !important;
99
+ box-shadow: var(--shadow-md) !important;
100
+ padding: 24px !important;
101
+ backdrop-filter: blur(18px) !important;
102
+ -webkit-backdrop-filter: blur(18px) !important;
103
+ transition: 0.25s ease;
104
+ }
105
+ .glass-card:hover {
106
+ background: var(--glass-hover) !important;
107
+ box-shadow: var(--shadow-lg) !important;
108
+ transform: translateY(-1px);
109
+ }
110
+
111
+ #status_column > * {
112
+ max-width: 100%;
113
+ }
114
+
115
+ /* The real fix: REMOVE overflow clipping on the Gradio column */
116
+ #status_column.gr-column {
117
+ overflow: visible !important;
118
+ }
119
+
120
+ /* Sticky fix β€” transfer scroll to viewport and enable sticky panel */
121
+ .gradio-container, .gr-block, .gr-blocks, .gr-row, .gr-column {
122
+ overflow: visible !important;
123
+ }
124
+
125
+ #status_column {
126
+ position: sticky !important;
127
+ top: 20px !important;
128
+ z-index: 999 !important;
129
+ height: max-content !important;
130
+ }
131
+
132
+ /* =========================================================
133
+ BUTTONS
134
+ ========================================================= */
135
+ button {
136
+ border-radius: 12px !important;
137
+ padding: 12px 24px !important;
138
+ font-weight: 600 !important;
139
+ transition: 0.25s ease;
140
+ }
141
+ button.primary {
142
+ background: linear-gradient(135deg, var(--glass-primary) 0%, var(--glass-primary-dark) 100%) !important;
143
+ border: 1px solid var(--glass-border) !important;
144
+ color: white !important;
145
+ box-shadow: var(--shadow-sm) !important;
146
+ }
147
+ button.primary:hover {
148
+ transform: translateY(-1px) scale(1.02);
149
+ box-shadow: var(--shadow-md) !important;
150
+ }
151
+ button.secondary {
152
+ background: var(--glass-bg) !important;
153
+ border: 1px solid var(--glass-border) !important;
154
+ color: var(--text-light) !important;
155
+ }
156
+ button.secondary:hover {
157
+ background: var(--glass-hover) !important;
158
+ }
159
+ button.approve-btn {
160
+ background: rgba(0, 180, 85, 0.68) !important;
161
+ border: 1px solid rgba(0, 255, 140, 0.45) !important;
162
+ color: white !important;
163
+ }
164
+ button.approve-btn:hover {
165
+ background: rgba(0, 205, 95, 0.85) !important;
166
+ }
167
+
168
+ button.reject-btn {
169
+ background: rgba(200, 38, 65, 0.7) !important;
170
+ border: 1px solid rgba(255, 90, 110, 0.5) !important;
171
+ color: white !important;
172
+ }
173
+ button.reject-btn:hover {
174
+ background: rgba(225, 45, 75, 0.86) !important;
175
+ }
176
+
177
+ /* =========================================================
178
+ INPUTS β€’ TEXTBOX β€’ DROPDOWN
179
+ ========================================================= */
180
+ input, textarea, select {
181
+ background: rgba(25, 25, 36, 0.55) !important;
182
+ border: 1px solid var(--glass-border) !important;
183
+ border-radius: 12px !important;
184
+ padding: 12px !important;
185
+ color: var(--text-light) !important;
186
+ backdrop-filter: blur(12px) !important;
187
+ }
188
+
189
+ /* FORCE TRUE DROPDOWN INSTEAD OF INPUT */
190
+ div[data-testid="dropdown"] input { display: none !important; }
191
+ div[data-testid="dropdown"] select { appearance: auto !important; cursor: pointer !important; }
192
+
193
+ /* =========================================================
194
+ FILE UPLOAD + LABEL BLACK BUG FIX
195
+ ========================================================= */
196
+ [data-testid*="file-upload"] {
197
+ background: rgba(25, 25, 36, 0.55) !important;
198
+ border: 2px dashed rgba(145, 115, 255, 0.55) !important;
199
+ border-radius: 18px !important;
200
+ backdrop-filter: blur(14px) !important;
201
+ }
202
+ [data-testid*="file-upload"] .label {
203
+ background: rgba(20, 20, 30, 0.75) !important;
204
+ border-radius: 10px !important;
205
+ padding: 6px 12px !important;
206
+ font-weight: 600;
207
+ }
208
+
209
+ /* =========================================================
210
+ SEPARATE SYSTEM PROMPT & ENV JSON VISUALLY
211
+ ========================================================= */
212
+ .gradio-container textarea:last-of-type {
213
+ margin-top: 12px !important;
214
+ }
215
+
216
+ /* =========================================================
217
+ CHAT AREA VISIBILITY
218
+ ========================================================= */
219
+ .gr-chatbot, .chatbot {
220
+ background: rgba(25, 25, 36, 0.45) !important;
221
+ border-radius: 20px !important;
222
+ backdrop-filter: blur(16px) !important;
223
+ }
224
+ .gr-chatbot .message {
225
+ color: var(--text-light) !important;
226
+ }
227
+
228
+ /* =========================================================
229
+ ACCORDION
230
+ ========================================================= */
231
+ .accordion {
232
+ background: var(--glass-bg) !important;
233
+ border: 1px solid var(--glass-border) !important;
234
+ border-radius: 18px !important;
235
+ backdrop-filter: blur(14px) !important;
236
+ }
237
+ .accordion button span {
238
+ color: var(--text-light) !important;
239
+ font-weight: 600;
240
+ }
241
+
242
+ /* =========================================================
243
+ SCROLLBAR
244
+ ========================================================= */
245
+ ::-webkit-scrollbar { width: 8px; }
246
+ ::-webkit-scrollbar-thumb {
247
+ background: rgba(140, 110, 255, 0.55);
248
+ border-radius: 10px;
249
+ }
250
+
251
+ /* =========================================================
252
+ RESPONSIVE
253
+ ========================================================= */
254
+ @media (max-width: 768px) {
255
+ .glass-card { padding: 18px !important; }
256
+ button { padding: 10px 20px !important; }
257
+ }
258
+
259
+ """
260
+
261
+ # JavaScript for localStorage API key management
262
+ STORAGE_JS = """
263
+ <script>
264
+ function saveApiKey(provider, key) {
265
+ if (key && key.trim()) {
266
+ localStorage.setItem('apiKey_' + provider, key.trim());
267
+ }
268
+ }
269
+
270
+ function loadApiKey(provider) {
271
+ return localStorage.getItem('apiKey_' + provider) || '';
272
+ }
273
+
274
+ function clearApiKey(provider) {
275
+ localStorage.removeItem('apiKey_' + provider);
276
+ }
277
+ </script>
278
+ """
279
+
280
+ # ============================================
281
+ # GLASSMORPHISM JAVASCRIPT ENHANCEMENTS
282
+ # ============================================
283
+ GLASSMORPHISM_JS = """
284
+ <script>
285
+ (function() {
286
+ 'use strict';
287
+
288
+ // Smooth scroll behavior
289
+ document.documentElement.style.scrollBehavior = 'smooth';
290
+
291
+ // Apply glass effects to dynamically loaded elements
292
+ function applyGlassEffect() {
293
+ // Target accordion elements
294
+ const accordions = document.querySelectorAll('.accordion');
295
+ accordions.forEach(acc => {
296
+ if (!acc.classList.contains('glass-processed')) {
297
+ acc.classList.add('glass-card');
298
+ acc.classList.add('glass-processed');
299
+ }
300
+ });
301
+
302
+ // Target file upload areas
303
+ const fileUploads = document.querySelectorAll('[data-testid="file-upload"]');
304
+ fileUploads.forEach(upload => {
305
+ if (!upload.classList.contains('file-upload-processed')) {
306
+ upload.classList.add('file-upload-container');
307
+ upload.classList.add('file-upload-processed');
308
+ }
309
+ });
310
+
311
+ // Fix dropdown visibility and styling
312
+ const dropdowns = document.querySelectorAll('.gr-dropdown, [role="listbox"]');
313
+ dropdowns.forEach(dropdown => {
314
+ if (!dropdown.classList.contains('dropdown-fixed')) {
315
+ dropdown.classList.add('dropdown-fixed');
316
+ dropdown.style.color = 'rgba(30, 30, 50, 0.95)';
317
+
318
+ // Fix all child elements
319
+ const children = dropdown.querySelectorAll('*');
320
+ children.forEach(child => {
321
+ child.style.color = 'rgba(30, 30, 50, 0.95)';
322
+ });
323
+ }
324
+ });
325
+
326
+ // Fix all labels
327
+ const labels = document.querySelectorAll('label, .label, span');
328
+ labels.forEach(label => {
329
+ if (!label.style.color || label.style.color === 'rgb(255, 255, 255)') {
330
+ label.style.color = 'rgba(30, 30, 50, 0.95)';
331
+ }
332
+ });
333
+ }
334
+
335
+ // Button ripple effect
336
+ function enhanceButtons() {
337
+ const buttons = document.querySelectorAll('button');
338
+ buttons.forEach(btn => {
339
+ if (btn.hasAttribute('data-ripple-enhanced')) return;
340
+ btn.setAttribute('data-ripple-enhanced', 'true');
341
+
342
+ btn.addEventListener('click', function(e) {
343
+ const ripple = document.createElement('span');
344
+ const rect = this.getBoundingClientRect();
345
+ const size = Math.max(rect.width, rect.height);
346
+ const x = e.clientX - rect.left - size / 2;
347
+ const y = e.clientY - rect.top - size / 2;
348
+
349
+ ripple.style.cssText = `
350
+ width: ${size}px;
351
+ height: ${size}px;
352
+ left: ${x}px;
353
+ top: ${y}px;
354
+ position: absolute;
355
+ border-radius: 50%;
356
+ background: rgba(255, 255, 255, 0.5);
357
+ transform: scale(0);
358
+ animation: ripple 0.6s ease-out;
359
+ pointer-events: none;
360
+ `;
361
+
362
+ this.style.position = 'relative';
363
+ this.style.overflow = 'hidden';
364
+ this.appendChild(ripple);
365
+
366
+ setTimeout(() => ripple.remove(), 600);
367
+ });
368
+ });
369
+ }
370
+
371
+ // Initialize enhancements
372
+ function init() {
373
+ setTimeout(() => {
374
+ applyGlassEffect();
375
+ enhanceButtons();
376
+ }, 500);
377
+
378
+ // Re-apply on Gradio updates
379
+ const observer = new MutationObserver(() => {
380
+ applyGlassEffect();
381
+ enhanceButtons();
382
+ });
383
+
384
+ observer.observe(document.body, {
385
+ childList: true,
386
+ subtree: true
387
+ });
388
+ }
389
 
390
+ // Add ripple animation CSS
391
+ const style = document.createElement('style');
392
+ style.textContent = `
393
+ @keyframes ripple {
394
+ to { transform: scale(4); opacity: 0; }
395
+ }
396
+ `;
397
+ document.head.appendChild(style);
398
+
399
+ // Run on load
400
+ if (document.readyState === 'loading') {
401
+ document.addEventListener('DOMContentLoaded', init);
402
+ } else {
403
+ init();
404
+ }
405
+ })();
406
+ </script>
407
+ """
408
+
409
+
410
+ # ----------------------------- STATUS UI -----------------------------
411
+ def format_status(message, status_type="info"):
412
+ icons = {"success": "βœ…", "error": "❌", "warning": "⚠️", "info": "ℹ️"}
413
+ return f"""
414
+ <div style="padding:15px;border-radius:12px;font-size:15px;background:rgba(255,255,255,.2);backdrop-filter:blur(12px);border:1px solid rgba(255,255,255,.4);">
415
+ <b>{icons.get(status_type,'ℹ️')} STATUS</b><br><br>{message}
416
+ </div>
417
+ """
418
+
419
+ # ----------------------------- BACKEND CALLS -----------------------------
420
  def start_session():
421
  global session_ok
422
  try:
423
  r = client.get(f"{API_URL}/session")
424
  if r.status_code == 200:
425
  session_ok = True
426
+ return format_status("Session started successfully!", "success")
427
+ return format_status(f"Failed to start session: {r.text}", "error")
428
  except Exception as e:
429
+ return format_status(f"Error: {str(e)}", "error")
430
 
431
 
432
  def upload_tools(file):
433
  global tools_ok
434
  if not session_ok:
435
+ return format_status("Start session first.", "warning")
436
+
437
  try:
438
+ r = client.post(f"{API_URL}/tools", files={"api": open(file, "rb")})
 
439
  if r.status_code == 200:
440
  tools_ok = True
441
+ return format_status("Tools generated from Postman collection!", "success")
442
+ return format_status(f"Upload failed: {r.text}", "error")
 
 
 
 
 
443
  except Exception as e:
444
+ return format_status(f"Error: {str(e)}", "error")
445
 
446
 
447
+ def initialize(system_prompt, env_vars, provider_display, api_key, model_name):
448
  global initialized
449
+
450
+ provider = next((id for id, p in PROVIDERS.items() if p["name"] == provider_display), None)
451
 
452
  try:
453
+ parsed = json.loads(env_vars) if env_vars.strip() else {}
454
  except Exception as e:
455
+ return format_status(f"Invalid JSON: {str(e)}", "error")
456
+
457
+ if PROVIDERS[provider]["requires_key"] and not api_key.strip():
458
+ return format_status("API key required for this provider.", "error")
459
 
460
  body = {
461
  "systemIntructions": system_prompt,
462
+ "envVariables": parsed,
463
+ "provider": provider,
464
+ "model": model_name,
465
+ "apiKey": api_key if api_key else None,
466
  }
467
+
468
  try:
469
  r = client.post(f"{API_URL}/initialize", json=body)
470
  if r.status_code == 200:
471
  initialized = True
472
+ return format_status("Agent initialized! Start chatting below.", "success")
473
+ return format_status(f"Initialization failed: {r.text}", "error")
474
  except Exception as e:
475
+ return format_status(f"Error: {str(e)}", "error")
 
 
 
 
 
 
 
476
 
 
 
 
 
 
 
 
 
 
 
477
 
478
+ # ----------------------------- CHAT LOGIC -----------------------------
479
+ def call_query(message, token):
480
+ headers = {"Authorization": f"Bearer {token}"} if token else {}
481
  try:
482
+ r = client.get(API_URL, params={"query": message}, headers=headers)
 
483
  body = r.json()
484
  return {"message": body.get("message", ""), "pending": body.get("data", [])}
 
 
485
  except Exception as e:
486
  return {"message": f"❌ Error: {str(e)}", "pending": []}
487
 
 
508
  # remove loading message safely
509
  history[-1] = (message, None)
510
 
511
+ # Check for rate limit error
512
+ if "Rate limit exceeded" in result.get("message", ""):
513
+ history[-1] = (message, f"❌ {result['message']}\n\nπŸ’‘ **Tip:** Switch to a paid provider with your own API key for unlimited requests!")
514
+ yield history, "", gr.update(visible=False), gr.update(visible=False), gr.update(visible=False)
515
+ return
516
+
517
  if result["pending"]:
518
  pending_requests = result["pending"]
519
  pending_auth_token = auth_token
 
530
 
531
  yield history, "", gr.update(visible=True), gr.update(visible=True), gr.update(visible=True)
532
  else:
533
+ history[-1] = (message, result["message"])
 
 
534
 
535
  yield history, "", gr.update(visible=False), gr.update(visible=False), gr.update(visible=False)
536
 
537
 
538
  def send_approval(approved, history):
539
  global pending_requests, pending_auth_token
 
540
  if not pending_requests:
541
+ history.append((None, "No pending approvals"))
542
+ return "Done", history, gr.update(visible=False), gr.update(visible=False), gr.update(visible=False)
543
 
544
+ data = [{"toolCallId": p["id"], "approved": approved} for p in pending_requests]
545
+ r = client.post(f"{API_URL}/approval", json=data,
546
+ headers={"Authorization": f"Bearer {pending_auth_token}"} if pending_auth_token else {})
547
+ pending_requests = []
548
+ history.append((None, r.json().get("message", "Done")))
549
+ return "Done", history, gr.update(visible=False), gr.update(visible=False), gr.update(visible=False)
 
 
 
 
 
 
 
 
 
 
 
550
 
551
 
552
+ def reset_chat(auth_token):
553
+ global pending_requests, pending_auth_token
554
+ # Attempt to reset session on backend
555
  try:
556
+ headers = {"Authorization": f"Bearer {auth_token}"} if auth_token else {}
557
+ client.delete(f"{API_URL}/session", headers=headers)
558
+ except Exception as e:
559
+ # Silently handle errors as reset should not fail the UI
560
  pass
561
+ # Reset only pending requests and chat UI, keep initialization
562
  pending_requests = []
563
  pending_auth_token = ""
564
  return [], gr.update(visible=False), gr.update(visible=False), gr.update(visible=False)
565
 
566
+ def toggle_provider_fields(provider_display):
567
+ provider_id = next(k for k,v in PROVIDERS.items() if v["name"] == provider_display)
568
+ return (
569
+ gr.update(visible=PROVIDERS[provider_id]["requires_key"]),
570
+ gr.update(visible=True, choices=MODEL_MAP[provider_id], value=MODEL_MAP[provider_id][0])
571
+ )
572
 
573
+ # ----------------------------- UI -----------------------------
574
+ with gr.Blocks(title="FluidTools",css=CUSTOM_CSS,
575
+ head=STORAGE_JS + GLASSMORPHISM_JS) as demo:
576
 
577
+ # Glassmorphic Hero Header
578
  gr.HTML("""
579
+ <div class="hero-header" style="
580
+ text-align: center;
581
+ padding: 48px 32px;
582
+ background: linear-gradient(135deg,
583
+ rgba(102, 126, 234, 0.7) 0%,
584
+ rgba(118, 75, 162, 0.8) 100%);
585
+ backdrop-filter: blur(16px);
586
+ -webkit-backdrop-filter: blur(16px);
587
+ color: white;
588
+ border-radius: 24px;
589
+ margin-bottom: 32px;
590
+ border: 1px solid rgba(255, 255, 255, 0.3);
591
+ box-shadow: 0 16px 32px rgba(0, 0, 0, 0.1), 0 0 40px rgba(102, 126, 234, 0.2);
592
+ position: relative;
593
+ overflow: hidden;
594
+ ">
595
+ <div style="position: relative; z-index: 2;">
596
+ <div style="display: inline-block; margin-bottom: 16px; font-size: 4em;
597
+ filter: drop-shadow(0 4px 12px rgba(0, 0, 0, 0.2));">
598
+ πŸ€–
599
+ </div>
600
+ <h1 style="
601
+ margin: 0;
602
+ font-size: 3em;
603
+ font-weight: 700;
604
+ letter-spacing: -1px;
605
+ text-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
606
+ ">FluidTools</h1>
607
+ <p style="
608
+ margin: 12px 0 0 0;
609
+ font-size: 1.3em;
610
+ opacity: 0.95;
611
+ font-weight: 400;
612
+ color: rgba(255, 255, 255, 0.9);
613
+ ">Turn any Postman API collection into an AI-powered chatbot</p>
614
+ <div style="
615
+ margin-top: 20px;
616
+ display: inline-flex;
617
+ gap: 12px;
618
+ padding: 8px 20px;
619
+ background: rgba(255, 255, 255, 0.15);
620
+ backdrop-filter: blur(8px);
621
+ -webkit-backdrop-filter: blur(8px);
622
+ border-radius: 100px;
623
+ border: 1px solid rgba(255, 255, 255, 0.2);
624
+ font-size: 0.9em;
625
+ ">
626
+ <span>✨ Multi-Provider</span>
627
+ <span style="opacity: 0.5;">|</span>
628
+ <span>πŸ›‘οΈ Human-in-the-Loop</span>
629
+ <span style="opacity: 0.5;">|</span>
630
+ <span>πŸš€ Zero Code</span>
631
+ </div>
632
+ </div>
633
+
634
+ <!-- Animated background circles -->
635
+ <div style="
636
+ position: absolute;
637
+ width: 300px;
638
+ height: 300px;
639
+ border-radius: 50%;
640
+ background: radial-gradient(circle, rgba(255, 255, 255, 0.1) 0%, transparent 70%);
641
+ top: -100px;
642
+ right: -100px;
643
+ z-index: 1;
644
+ animation: float 6s ease-in-out infinite;
645
+ "></div>
646
+ <div style="
647
+ position: absolute;
648
+ width: 200px;
649
+ height: 200px;
650
+ border-radius: 50%;
651
+ background: radial-gradient(circle, rgba(255, 255, 255, 0.08) 0%, transparent 70%);
652
+ bottom: -50px;
653
+ left: -50px;
654
+ z-index: 1;
655
+ animation: float 8s ease-in-out infinite reverse;
656
+ "></div>
657
  </div>
658
  """)
659
 
 
662
  1. **Upload** your Postman collection (JSON export from Postman)
663
  2. **Initialize** the AI agent with your API tools
664
  3. **Chat** with your APIs using natural language - no coding required!
665
+ """, elem_classes=["glass-card"])
666
+
667
+
668
+ # --- SECTION LAYOUT ---
669
+ with gr.Row():
670
+
671
+ # ---- LEFT: STEPS AREA (70%) ----
672
+ with gr.Column(scale=7):
673
+
674
+ with gr.Accordion("πŸš€ STEP 1 β€” Initialize System", open=True):
675
+
676
+ start_btn = gr.Button("πŸ§ͺ Start Session")
677
+ file_in = gr.File(file_types=[".json"])
678
+ provider_selector = gr.Dropdown(
679
+ choices=[p["name"] for p in PROVIDERS.values()],
680
+ value="πŸ†“ Nebius Free (10 requests/day)",
681
+ label="Provider",
682
+ )
683
+ model_dropdown = gr.Dropdown(
684
+ label="Model",
685
+ choices=[],
686
+ visible=False
687
+ )
688
+ api_key_box = gr.Textbox(label="API Key (required for paid providers)", type="password", visible=False)
689
+ sys_prompt = gr.Textbox(label="System Prompt", value=DEMO_SYSTEM_PROMPT if DEMO_MODE else "")
690
+ env_vars = gr.Textbox(label="Environment Variables JSON", value=DEMO_ENV_VARS if DEMO_MODE else "")
691
+ init_btn = gr.Button("πŸš€ Initialize Agent", interactive=False)
692
+
693
+ with gr.Accordion("πŸ’¬ STEP 2 β€” Chat with API", open=False):
694
+ auth_box = gr.Textbox(label="Auth Token", type="password")
695
+ chat = gr.Chatbot(height=450)
696
+ msg = gr.Textbox()
697
+ send = gr.Button("πŸ“€ Send")
698
+ reset = gr.Button("πŸ”„ Reset Conversation")
699
+
700
+ approval_md = gr.Markdown(visible=False)
701
+ approve_btn = gr.Button("Approve All", visible=False, elem_classes=["approve-btn"])
702
+ reject_btn = gr.Button("Reject All", visible=False, elem_classes=["reject-btn"])
703
+ approval_result = gr.Textbox(visible=False)
704
+
705
+ # ---- RIGHT: STATUS AREA (30%) ----
706
+ with gr.Column(scale=3, elem_id="status_column"):
707
+ status_box = gr.HTML(format_status("Awaiting Session Start"))
708
 
709
+ gr.HTML("""
710
+ <div style="
711
+ margin-top: 32px;
712
+ padding: 32px;
713
+ text-align: center;
714
+ background: rgba(255, 255, 255, 0.08);
715
+ backdrop-filter: blur(14px);
716
+ -webkit-backdrop-filter: blur(14px);
717
+ border-radius: 20px;
718
+ border: 1px solid rgba(255, 255, 255, 0.25);
719
+ box-shadow: 0 10px 30px rgba(0,0,0,0.18);
720
+ ">
721
+
722
+ <h2 style="font-size: 1.9em; margin-bottom: 20px; font-weight: 700;">
723
+ πŸ“š Learn More
724
+ </h2>
725
+
726
+ <div style="
727
+ display: flex;
728
+ justify-content: center;
729
+ gap: 40px;
730
+ flex-wrap: wrap;
731
+ margin-top: 10px;
732
+ ">
733
+ <!-- GitHub -->
734
+ <a href="https://github.com/KartikJoshiUK/fluidtools" target="_blank">
735
+ <img
736
+ src="https://cdn.jsdelivr.net/npm/simple-icons@v11/icons/github.svg"
737
+ style="height: 50px; filter: drop-shadow(0px 0px 10px rgba(200,200,255,0.5)); transition: 0.25s;">
738
+ </a>
739
+
740
+ <!-- NPM -->
741
+ <a href="https://www.npmjs.com/package/fluidtools" target="_blank">
742
+ <img
743
+ src="https://cdn.jsdelivr.net/npm/simple-icons@v11/icons/npm.svg"
744
+ style="height: 50px; filter: drop-shadow(0px 0px 10px rgba(255,120,120,0.55)); transition: 0.25s;">
745
+ </a>
746
+ </div>
747
+
748
+ <div style="margin: 26px 0; height: 2px; width: 75%;
749
+ background: linear-gradient(90deg, transparent, rgba(255,255,255,0.4), transparent);
750
+ margin-left:auto; margin-right:auto;"></div>
751
+
752
+ <h3 style="margin-bottom: 18px; font-size: 1.6em; font-weight: 700;">
753
+ 🌟 Sponsors
754
+ </h3>
755
+
756
+ <div style="
757
+ display: flex;
758
+ justify-content: center;
759
+ gap: 45px;
760
+ flex-wrap: wrap;
761
+ margin-top: 18px;
762
+ ">
763
+
764
+ <!-- Each logo forced to uniform height = 62px while preserving proportions -->
765
+ <a href="https://www.gradio.app/" target="_blank">
766
+ <img src="https://www.gradio.app/_app/immutable/assets/gradiodark.CbgYRzQH.svg"
767
+ style="height: 62px; object-fit: contain; filter: drop-shadow(0px 0px 10px rgba(140,110,255,0.65)); transition: 0.25s;">
768
+ </a>
769
+
770
+ <a href="https://nebius.com/" target="_blank">
771
+ <img src="https://nebius.com/logo.svg"
772
+ style="height: 62px; object-fit: contain; filter: drop-shadow(0px 0px 10px rgba(110,190,255,0.6)); transition: 0.25s;">
773
+ </a>
774
+
775
+ <a href="https://modal.com/" target="_blank">
776
+ <img src="https://modal.com/_app/immutable/assets/logo.lottie.CgmMXf1s.png"
777
+ style="height: 62px; object-fit: contain; filter: drop-shadow(0px 0px 10px rgba(255,105,95,0.6)); transition: 0.25s;">
778
+ </a>
779
+ </div>
780
+
781
+ <div style="margin-top: 26px; font-size: 0.95em; opacity: 0.85;">
782
+ FluidTools ecosystem β€’ Multi-provider LLM support (OpenAI, Anthropic, Gemini, Nebius) β€’ Human-in-the-loop safety β€’ Semantic tool selection
783
+ </div>
784
+ </div>
785
  """)
786
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
787
 
 
 
 
788
 
789
+ # INTERACTIONS
790
+ def toggle_api(provider_display):
791
+ provider_id = next(k for k,v in PROVIDERS.items() if v["name"] == provider_display)
792
+ return gr.update(visible=PROVIDERS[provider_id]["requires_key"])
793
 
794
+ provider_selector.change(toggle_api, provider_selector, api_key_box)
795
+ provider_selector.change(
796
+ toggle_provider_fields,
797
+ provider_selector,
798
+ [api_key_box, model_dropdown]
799
+ )
800
 
801
+ def refresh_buttons():
802
  return gr.update(interactive=session_ok), gr.update(interactive=(session_ok and tools_ok))
803
 
804
+ start_btn.click(start_session, None, status_box).then(refresh_buttons, None, [file_in, init_btn])
805
+ file_in.upload(upload_tools, file_in, status_box).then(refresh_buttons, None, [file_in, init_btn])
806
+ init_btn.click(initialize, [sys_prompt, env_vars, provider_selector, api_key_box, model_dropdown], status_box)
807
 
808
+ send.click(chat_send, [msg, chat, auth_box], [chat, msg, approval_md, approve_btn, reject_btn])
809
+ msg.submit(chat_send, [msg, chat, auth_box], [chat, msg, approval_md, approve_btn, reject_btn])
810
 
811
+ approve_btn.click(send_approval, [gr.State(True), chat], [approval_result, chat, approval_md, approve_btn, reject_btn])
812
+ reject_btn.click(send_approval, [gr.State(False), chat], [approval_result, chat, approval_md, approve_btn, reject_btn])
813
 
814
+ reset.click(reset_chat, auth_box, [chat, approval_md, approve_btn, reject_btn])
 
815
 
 
816
 
817
  if __name__ == "__main__":
818
+ demo.launch(server_port=7860, share=False)