flock-core 0.4.0b48__py3-none-any.whl → 0.4.0b50__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of flock-core might be problematic. Click here for more details.

Files changed (32) hide show
  1. flock/__init__.py +45 -3
  2. flock/modules/mem0/mem0_module.py +63 -0
  3. flock/modules/mem0graph/__init__.py +1 -0
  4. flock/modules/mem0graph/mem0_graph_module.py +63 -0
  5. flock/webapp/app/api/execution.py +105 -47
  6. flock/webapp/app/chat.py +315 -24
  7. flock/webapp/app/config.py +15 -1
  8. flock/webapp/app/dependencies.py +22 -0
  9. flock/webapp/app/main.py +414 -14
  10. flock/webapp/app/services/flock_service.py +38 -13
  11. flock/webapp/app/services/sharing_models.py +43 -0
  12. flock/webapp/app/services/sharing_store.py +156 -0
  13. flock/webapp/static/css/chat.css +57 -0
  14. flock/webapp/templates/base.html +91 -1
  15. flock/webapp/templates/chat.html +93 -5
  16. flock/webapp/templates/partials/_agent_detail_form.html +3 -3
  17. flock/webapp/templates/partials/_chat_messages.html +1 -1
  18. flock/webapp/templates/partials/_chat_settings_form.html +22 -0
  19. flock/webapp/templates/partials/_execution_form.html +28 -1
  20. flock/webapp/templates/partials/_flock_properties_form.html +2 -2
  21. flock/webapp/templates/partials/_results_display.html +15 -11
  22. flock/webapp/templates/partials/_share_chat_link_snippet.html +11 -0
  23. flock/webapp/templates/partials/_share_link_snippet.html +35 -0
  24. flock/webapp/templates/partials/_structured_data_view.html +2 -2
  25. flock/webapp/templates/shared_run_page.html +143 -0
  26. {flock_core-0.4.0b48.dist-info → flock_core-0.4.0b50.dist-info}/METADATA +4 -2
  27. {flock_core-0.4.0b48.dist-info → flock_core-0.4.0b50.dist-info}/RECORD +31 -24
  28. flock/modules/zep/zep_module.py +0 -187
  29. /flock/modules/{zep → mem0}/__init__.py +0 -0
  30. {flock_core-0.4.0b48.dist-info → flock_core-0.4.0b50.dist-info}/WHEEL +0 -0
  31. {flock_core-0.4.0b48.dist-info → flock_core-0.4.0b50.dist-info}/entry_points.txt +0 -0
  32. {flock_core-0.4.0b48.dist-info → flock_core-0.4.0b50.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,156 @@
1
+ import logging
2
+ import sqlite3
3
+ from abc import ABC, abstractmethod
4
+ from pathlib import Path
5
+
6
+ import aiosqlite
7
+
8
+ from flock.webapp.app.services.sharing_models import SharedLinkConfig
9
+
10
+ # Get a logger instance
11
+ logger = logging.getLogger(__name__)
12
+
13
+ class SharedLinkStoreInterface(ABC):
14
+ """Interface for storing and retrieving shared link configurations."""
15
+
16
+ @abstractmethod
17
+ async def initialize(self) -> None:
18
+ """Initialize the store (e.g., create tables)."""
19
+ pass
20
+
21
+ @abstractmethod
22
+ async def save_config(self, config: SharedLinkConfig) -> SharedLinkConfig:
23
+ """Saves a shared link configuration."""
24
+ pass
25
+
26
+ @abstractmethod
27
+ async def get_config(self, share_id: str) -> SharedLinkConfig | None:
28
+ """Retrieves a shared link configuration by its ID."""
29
+ pass
30
+
31
+ @abstractmethod
32
+ async def delete_config(self, share_id: str) -> bool:
33
+ """Deletes a shared link configuration by its ID. Returns True if deleted, False otherwise."""
34
+ pass
35
+
36
+ class SQLiteSharedLinkStore(SharedLinkStoreInterface):
37
+ """SQLite implementation for storing and retrieving shared link configurations."""
38
+
39
+ def __init__(self, db_path: str):
40
+ self.db_path = Path(db_path)
41
+ self.db_path.parent.mkdir(parents=True, exist_ok=True) # Ensure directory exists
42
+ logger.info(f"SQLiteSharedLinkStore initialized with db_path: {self.db_path}")
43
+
44
+ async def initialize(self) -> None:
45
+ """Initializes the database and creates/updates the table if it doesn't exist."""
46
+ try:
47
+ async with aiosqlite.connect(self.db_path) as db:
48
+ # Ensure the table exists with the base schema first
49
+ await db.execute(
50
+ """
51
+ CREATE TABLE IF NOT EXISTS shared_links (
52
+ share_id TEXT PRIMARY KEY,
53
+ agent_name TEXT NOT NULL,
54
+ flock_definition TEXT NOT NULL,
55
+ created_at TEXT NOT NULL
56
+ /* New columns will be added below if they don't exist */
57
+ )
58
+ """
59
+ )
60
+
61
+ # Add new columns individually, ignoring errors if they already exist
62
+ new_columns = [
63
+ ("share_type", "TEXT DEFAULT 'agent_run' NOT NULL"),
64
+ ("chat_message_key", "TEXT"),
65
+ ("chat_history_key", "TEXT"),
66
+ ("chat_response_key", "TEXT")
67
+ ]
68
+
69
+ for column_name, column_type in new_columns:
70
+ try:
71
+ await db.execute(f"ALTER TABLE shared_links ADD COLUMN {column_name} {column_type}")
72
+ logger.info(f"Added column '{column_name}' to shared_links table.")
73
+ except sqlite3.OperationalError as e:
74
+ if "duplicate column name" in str(e).lower():
75
+ logger.debug(f"Column '{column_name}' already exists in shared_links table.")
76
+ else:
77
+ raise # Re-raise if it's a different operational error
78
+
79
+ await db.commit()
80
+ logger.info(f"Database initialized and shared_links table schema ensured at {self.db_path}")
81
+ except sqlite3.Error as e:
82
+ logger.error(f"SQLite error during initialization: {e}", exc_info=True)
83
+ raise
84
+
85
+ async def save_config(self, config: SharedLinkConfig) -> SharedLinkConfig:
86
+ """Saves a shared link configuration to the SQLite database."""
87
+ try:
88
+ async with aiosqlite.connect(self.db_path) as db:
89
+ await db.execute(
90
+ """INSERT INTO shared_links (
91
+ share_id, agent_name, created_at, flock_definition,
92
+ share_type, chat_message_key, chat_history_key, chat_response_key
93
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)""",
94
+ (
95
+ config.share_id,
96
+ config.agent_name,
97
+ config.created_at.isoformat(),
98
+ config.flock_definition,
99
+ config.share_type,
100
+ config.chat_message_key,
101
+ config.chat_history_key,
102
+ config.chat_response_key,
103
+ ),
104
+ )
105
+ await db.commit()
106
+ logger.info(f"Saved shared link config for ID: {config.share_id} with type: {config.share_type}")
107
+ return config
108
+ except sqlite3.Error as e:
109
+ logger.error(f"SQLite error saving config for ID {config.share_id}: {e}", exc_info=True)
110
+ raise
111
+
112
+ async def get_config(self, share_id: str) -> SharedLinkConfig | None:
113
+ """Retrieves a shared link configuration from SQLite by its ID."""
114
+ try:
115
+ async with aiosqlite.connect(self.db_path) as db:
116
+ async with db.execute(
117
+ """SELECT
118
+ share_id, agent_name, created_at, flock_definition,
119
+ share_type, chat_message_key, chat_history_key, chat_response_key
120
+ FROM shared_links WHERE share_id = ?""",
121
+ (share_id,)
122
+ ) as cursor:
123
+ row = await cursor.fetchone()
124
+ if row:
125
+ logger.debug(f"Retrieved shared link config for ID: {share_id}")
126
+ return SharedLinkConfig(
127
+ share_id=row[0],
128
+ agent_name=row[1],
129
+ created_at=row[2], # SQLite stores as TEXT, Pydantic will parse from ISO format
130
+ flock_definition=row[3],
131
+ share_type=row[4],
132
+ chat_message_key=row[5],
133
+ chat_history_key=row[6],
134
+ chat_response_key=row[7],
135
+ )
136
+ logger.debug(f"No shared link config found for ID: {share_id}")
137
+ return None
138
+ except sqlite3.Error as e:
139
+ logger.error(f"SQLite error retrieving config for ID {share_id}: {e}", exc_info=True)
140
+ return None # Or raise, depending on desired error handling
141
+
142
+ async def delete_config(self, share_id: str) -> bool:
143
+ """Deletes a shared link configuration from SQLite by its ID."""
144
+ try:
145
+ async with aiosqlite.connect(self.db_path) as db:
146
+ result = await db.execute("DELETE FROM shared_links WHERE share_id = ?", (share_id,))
147
+ await db.commit()
148
+ deleted_count = result.rowcount
149
+ if deleted_count > 0:
150
+ logger.info(f"Deleted shared link config for ID: {share_id}")
151
+ return True
152
+ logger.info(f"Attempted to delete non-existent shared link config for ID: {share_id}")
153
+ return False
154
+ except sqlite3.Error as e:
155
+ logger.error(f"SQLite error deleting config for ID {share_id}: {e}", exc_info=True)
156
+ return False # Or raise
@@ -240,3 +240,60 @@ body:not(.chat-page) .chat-settings-form .grid button:first-child {
240
240
  main.main-content:has(#chat-container) {
241
241
  overflow: hidden;
242
242
  }
243
+
244
+ /* Ensure all direct text and common markdown elements within bot bubbles use the bot's text color */
245
+ .bubble.bot,
246
+ .bubble.bot p,
247
+ .bubble.bot li,
248
+ .bubble.bot h1,
249
+ .bubble.bot h2,
250
+ .bubble.bot h3,
251
+ .bubble.bot h4,
252
+ .bubble.bot h5,
253
+ .bubble.bot h6,
254
+ .bubble.bot strong,
255
+ .bubble.bot em,
256
+ .bubble.bot table,
257
+ .bubble.bot th,
258
+ .bubble.bot td {
259
+ color: var(--pico-code-color);
260
+ }
261
+
262
+ /* For links specifically within bot messages, you might want them to also use the bot text color */
263
+ .bubble.bot a {
264
+ color: var(--pico-code-color);
265
+ text-decoration: underline; /* Or your preferred link style */
266
+ }
267
+ .bubble.bot a:hover {
268
+ color: var(--pico-primary-hover, var(--pico-primary));
269
+ text-decoration: underline;
270
+ }
271
+
272
+ /* Styling for code blocks generated by Markdown and highlighted by Prism.js */
273
+ /* The prism-okaidia theme will handle the internal colors of the code. */
274
+ /* This is more about the container of the code block. */
275
+ .bubble.bot pre[class*="language-"] {
276
+ background-color: var(--pico-card-background-color); /* Or a slightly different dark shade */
277
+ border: 1px solid var(--pico-muted-border-color);
278
+ border-radius: var(--pico-border-radius);
279
+ padding: 0.75em;
280
+ margin: 0.5em 0;
281
+ font-size: 0.875em; /* Adjust as needed */
282
+ overflow-x: auto; /* Allow horizontal scrolling for long code lines */
283
+ /* The text color *inside* the code block will be handled by the Prism theme (e.g., Okaidia) */
284
+ }
285
+
286
+ /* Ensure the code itself inside the pre block also resets its base color if needed,
287
+ though Prism themes usually take care of this. This is a fallback. */
288
+ .bubble.bot pre[class*="language-"] code {
289
+ /* color: inherit; */ /* This might not be necessary if Prism theme is comprehensive - try without first */
290
+ background: none;
291
+ padding: 0;
292
+ }
293
+
294
+ /* Styles for preformatted text wrapping */
295
+ .bubble pre {
296
+ white-space: pre-wrap; /* Allows wrapping and preserves sequences of white space and newlines */
297
+ word-wrap: break-word; /* Breaks long words/strings if they would overflow */
298
+ overflow-x: auto; /* Adds a scrollbar if content is still too wide */
299
+ }
@@ -14,8 +14,10 @@
14
14
  <link rel="stylesheet" href="/static/css/chat.css">
15
15
  <!-- Font Awesome for icons -->
16
16
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
17
+ <!-- Prism.js CSS for syntax highlighting (okaidia theme) -->
18
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-okaidia.min.css" referrerpolicy="no-referrer" />
17
19
  {# Inject Theme CSS Variables #}
18
- {% if theme_css %}
20
+ {% if theme_css and theme_css.strip() %}
19
21
  <style>
20
22
  /* Start Theme CSS */
21
23
  {{ theme_css | safe }}
@@ -27,10 +29,17 @@
27
29
  integrity="sha384-D1Kt99CQMDuVetoL1lrYwg5t+9QdHe7NLX/SoJYkXDFfX37iInKRy5xLSi8nO7UC"
28
30
  crossorigin="anonymous"></script>
29
31
  <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
32
+ <!-- Prism.js JS (core and autoloader) -->
33
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-core.min.js" referrerpolicy="no-referrer"></script>
34
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/autoloader/prism-autoloader.min.js" referrerpolicy="no-referrer"></script>
30
35
  <!-- Removed inline styles as they're in custom.css -->
31
36
  </head>
32
37
 
33
38
  <body>
39
+ <div id="global-toast-container" style="position: fixed; top: 1rem; right: 1rem; z-index: 9999; display: flex; flex-direction: column; gap: 0.5rem;">
40
+ <!-- Toasts will be appended here -->
41
+ </div>
42
+
34
43
  <header class="top-header">
35
44
  <span><strong>🐧 Flock Playground 🐤</strong></span>
36
45
  <span id="header-flock-status-container" hx-get="/ui/htmx/header-flock-status?ui_mode={{ ui_mode }}"
@@ -104,6 +113,87 @@
104
113
  function triggerEvent(eventName, detail = {}) {
105
114
  htmx.trigger(document.body, eventName, detail);
106
115
  }
116
+
117
+ // Add HTMX event listener for Prism highlighting
118
+ document.addEventListener('htmx:afterSwap', function(event) {
119
+ // Check if the swapped element or its parent is the main content area or results display
120
+ const mainContentArea = document.getElementById('main-content-area');
121
+ const resultsDisplay = document.getElementById('results-display-content');
122
+ let targetElement = event.detail.target;
123
+
124
+ if (targetElement === mainContentArea || (resultsDisplay && resultsDisplay.contains(targetElement)) || (mainContentArea && mainContentArea.contains(targetElement))) {
125
+ if (typeof Prism !== 'undefined') {
126
+ // console.log('Prism highlighting triggered for swapped content in main area.');
127
+ Prism.highlightAllUnder(mainContentArea || document.documentElement);
128
+ }
129
+ }
130
+ });
131
+
132
+ // Initial highlight on page load
133
+ document.addEventListener('DOMContentLoaded', () => {
134
+ if (typeof Prism !== 'undefined') {
135
+ // console.log('Prism initial highlighting.');
136
+ Prism.highlightAll();
137
+ }
138
+ });
139
+ </script>
140
+ {# Global Toast Handler - identical to the one in chat.html #}
141
+ <script>
142
+ document.body.addEventListener('showGlobalToast', function (event) {
143
+ const detail = event.detail;
144
+ const message = detail.message || 'Done!';
145
+ const type = detail.type || 'info'; // success, error, warning, info
146
+
147
+ const toastContainer = document.getElementById('global-toast-container');
148
+ if (!toastContainer) return;
149
+
150
+ const toast = document.createElement('article');
151
+ toast.className = `toast-message ${type}`;
152
+ toast.innerHTML = `<button class="close-toast" style="float: right; background: none; border: none; font-size: 1.2rem; line-height: 1; padding: 0.25rem 0.5rem; cursor: pointer;">&times;</button>${message}`;
153
+
154
+ toast.style.padding = '0.75rem';
155
+ toast.style.border = '1px solid';
156
+ toast.style.borderRadius = 'var(--pico-border-radius, 4px)';
157
+ toast.style.minWidth = '250px';
158
+ toast.style.maxWidth = '400px';
159
+ toast.style.opacity = '0';
160
+ toast.style.transition = 'opacity 0.3s ease-in-out, transform 0.3s ease-in-out';
161
+ toast.style.transform = 'translateX(100%)';
162
+
163
+ if (type === 'success') {
164
+ toast.style.borderColor = 'var(--pico-color-green-500, green)';
165
+ toast.style.backgroundColor = 'var(--pico-color-green-150, #e6fffa)';
166
+ toast.style.color = 'var(--pico-color-green-700, darkgreen)';
167
+ } else if (type === 'error') {
168
+ toast.style.borderColor = 'var(--pico-color-red-500, red)';
169
+ toast.style.backgroundColor = 'var(--pico-color-red-150, #ffe6e6)';
170
+ toast.style.color = 'var(--pico-color-red-700, darkred)';
171
+ } else { // Default/info/warning
172
+ toast.style.borderColor = 'var(--pico-color-blue-500, blue)';
173
+ toast.style.backgroundColor = 'var(--pico-color-blue-150, #e6f7ff)';
174
+ toast.style.color = 'var(--pico-color-blue-700, darkblue)';
175
+ }
176
+
177
+ toastContainer.appendChild(toast);
178
+
179
+ setTimeout(() => {
180
+ toast.style.opacity = '1';
181
+ toast.style.transform = 'translateX(0)';
182
+ }, 10);
183
+
184
+ const closeButton = toast.querySelector('.close-toast');
185
+ closeButton.onclick = () => {
186
+ toast.style.opacity = '0';
187
+ toast.style.transform = 'translateY(-20px)';
188
+ setTimeout(() => toast.remove(), 300);
189
+ };
190
+
191
+ setTimeout(() => {
192
+ toast.style.opacity = '0';
193
+ toast.style.transform = 'translateY(-20px)';
194
+ setTimeout(() => toast.remove(), 300);
195
+ }, 5000);
196
+ });
107
197
  </script>
108
198
  </body>
109
199
 
@@ -6,8 +6,10 @@
6
6
  <title>Flock Chat</title>
7
7
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css" />
8
8
  <link rel="stylesheet" href="/static/css/chat.css">
9
+ {# Prism.js CSS for syntax highlighting (okaidia theme) #}
10
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-okaidia.min.css" referrerpolicy="no-referrer" />
9
11
  {# Inject active theme variables #}
10
- {% if theme_css %}
12
+ {% if theme_css and theme_css.strip() %}
11
13
  <style>
12
14
  /* Start Theme CSS */
13
15
  /* stylelint-disable */
@@ -17,29 +19,48 @@
17
19
  </style>
18
20
  {% endif %}
19
21
  <script src="https://unpkg.com/htmx.org@1.9.10" integrity="sha384-D1Kt99CQMDuVetoL1lrYwg5t+9QdHe7NLX/SoJYkXDFfX37iInKRy5xLSi8nO7UC" crossorigin="anonymous"></script>
22
+ {# Prism.js JS (core and components for common languages) #}
23
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-core.min.js" referrerpolicy="no-referrer"></script>
24
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/autoloader/prism-autoloader.min.js" referrerpolicy="no-referrer"></script>
20
25
  </head>
21
26
  <body class="chat-page">
27
+ <div id="global-toast-container" style="position: fixed; top: 1rem; right: 1rem; z-index: 9999; display: flex; flex-direction: column; gap: 0.5rem;">
28
+ <!-- Toasts will be appended here -->
29
+ </div>
30
+
22
31
  <div id="chat-container">
23
32
  <div class="chat-header" style="justify-content: space-between;">
24
33
  <hgroup>
25
34
  <h2>Flock Chat</h2>
26
35
  <h3>{{ chat_subtitle }}</h3>
27
36
  </hgroup>
28
- <button class="secondary outline" hx-get="/chat/htmx/settings-form" hx-target="#chat-content-area" hx-swap="innerHTML" style="min-width:auto;">Settings</button>
37
+ {% if not is_shared_chat %}
38
+ <button class="secondary outline" hx-get="/chat/settings-standalone" hx-target="#chat-content-area" hx-swap="innerHTML" style="min-width:auto;">Settings</button>
39
+ {% else %}
40
+ {# Optionally, could show a disabled settings button or some other indicator #}
41
+ <span style="font-size: 0.8rem; color: var(--pico-muted-color);">Settings frozen for shared session.</span>
42
+ {% endif %}
29
43
  </div>
30
44
 
31
45
  <div id="chat-content-area">
32
- <div id="chat-log" hx-get="/chat/messages" hx-trigger="load" hx-swap="innerHTML">
46
+ <div id="chat-log"
47
+ hx-get="{% if is_shared_chat %}/chat/messages-shared/{{ share_id }}{% else %}/chat/messages{% endif %}"
48
+ hx-trigger="load" {# Polling removed #}
49
+ hx-swap="innerHTML">
33
50
  <p><em>Loading chat…</em></p>
34
51
  </div>
35
52
 
36
53
  <form id="chat-form-standalone"
37
- hx-post="/chat/send"
54
+ hx-post="{% if is_shared_chat %}/chat/send-shared{% else %}/chat/send{% endif %}"
38
55
  hx-target="#chat-log"
39
56
  hx-swap="innerHTML"
40
57
  hx-disabled-elt="input[name='message'], button[type='submit']"
41
58
  hx-on::before-request="htmx.find('#chat-form-standalone button[type=\'submit\']').textContent = 'Sending...'"
42
59
  hx-on::after-request="htmx.find('#chat-form-standalone button[type=\'submit\']').textContent = 'Send'; this.reset();">
60
+
61
+ {% if is_shared_chat %}
62
+ <input type="hidden" name="share_id" value="{{ share_id }}">
63
+ {% endif %}
43
64
  <input type="text" name="message" placeholder="Type a message…" required autofocus>
44
65
  <button type="submit">Send</button>
45
66
  </form>
@@ -57,10 +78,77 @@
57
78
  log.scrollTop = log.scrollHeight;
58
79
  }
59
80
  document.addEventListener('htmx:afterSwap', e => {
60
- if (e.detail.target.id === 'chat-log') scrollBottom();
81
+ if (e.detail.target.id === 'chat-log') {
82
+ scrollBottom();
83
+ // Re-run Prism highlighting after new content is swapped in
84
+ Prism.highlightAllUnder(log);
85
+ }
61
86
  });
62
87
  window.addEventListener('load', scrollBottom);
88
+ // Initial highlight on page load for any pre-existing content
89
+ document.addEventListener('DOMContentLoaded', () => {
90
+ Prism.highlightAll();
91
+ });
63
92
  })();
93
+
94
+ // Global Toast Handler
95
+ document.body.addEventListener('showGlobalToast', function (event) {
96
+ const detail = event.detail;
97
+ const message = detail.message || 'Done!';
98
+ const type = detail.type || 'info'; // success, error, warning, info
99
+
100
+ const toastContainer = document.getElementById('global-toast-container');
101
+ if (!toastContainer) return;
102
+
103
+ const toast = document.createElement('article');
104
+ toast.className = `toast-message ${type}`;
105
+ toast.innerHTML = `<button class="close-toast" style="float: right; background: none; border: none; font-size: 1.2rem; line-height: 1; padding: 0.25rem 0.5rem; cursor: pointer;">&times;</button>${message}`;
106
+
107
+ // Basic styling, can be enhanced in CSS file
108
+ toast.style.padding = '0.75rem';
109
+ toast.style.border = '1px solid';
110
+ toast.style.borderRadius = 'var(--pico-border-radius, 4px)';
111
+ toast.style.minWidth = '250px';
112
+ toast.style.maxWidth = '400px';
113
+ toast.style.opacity = '0'; // Start faded out
114
+ toast.style.transition = 'opacity 0.3s ease-in-out, transform 0.3s ease-in-out';
115
+ toast.style.transform = 'translateX(100%)'; // Start off-screen
116
+
117
+ if (type === 'success') {
118
+ toast.style.borderColor = 'var(--pico-color-green-500, green)';
119
+ toast.style.backgroundColor = 'var(--pico-color-green-150, #e6fffa)';
120
+ toast.style.color = 'var(--pico-color-green-700, darkgreen)';
121
+ } else if (type === 'error') {
122
+ toast.style.borderColor = 'var(--pico-color-red-500, red)';
123
+ toast.style.backgroundColor = 'var(--pico-color-red-150, #ffe6e6)';
124
+ toast.style.color = 'var(--pico-color-red-700, darkred)';
125
+ } else { // Default/info/warning
126
+ toast.style.borderColor = 'var(--pico-color-blue-500, blue)';
127
+ toast.style.backgroundColor = 'var(--pico-color-blue-150, #e6f7ff)';
128
+ toast.style.color = 'var(--pico-color-blue-700, darkblue)';
129
+ }
130
+
131
+ toastContainer.appendChild(toast);
132
+
133
+ // Animate in
134
+ setTimeout(() => {
135
+ toast.style.opacity = '1';
136
+ toast.style.transform = 'translateX(0)';
137
+ }, 10); // Small delay to allow CSS to apply initial state
138
+
139
+ const closeButton = toast.querySelector('.close-toast');
140
+ closeButton.onclick = () => {
141
+ toast.style.opacity = '0';
142
+ toast.style.transform = 'translateY(-20px)';
143
+ setTimeout(() => toast.remove(), 300);
144
+ };
145
+
146
+ setTimeout(() => {
147
+ toast.style.opacity = '0';
148
+ toast.style.transform = 'translateY(-20px)';
149
+ setTimeout(() => toast.remove(), 300);
150
+ }, 5000); // Auto-dismiss after 5 seconds
151
+ });
64
152
  </script>
65
153
  </body>
66
154
  </html>
@@ -14,9 +14,9 @@
14
14
  {% endif %}
15
15
 
16
16
  <form {% if is_new %}
17
- hx-post="/api/flock/htmx/agents"
17
+ hx-post="/ui/api/flock/htmx/agents"
18
18
  {% else %}
19
- hx-put="/api/flock/htmx/agents/{{ agent.name if agent else '' }}"
19
+ hx-put="/ui/api/flock/htmx/agents/{{ agent.name if agent else '' }}"
20
20
  {% endif %}
21
21
  hx-target="#agent-detail-form-content"
22
22
  hx-swap="innerHTML"
@@ -77,7 +77,7 @@
77
77
  </button>
78
78
  {% if not is_new and agent %}
79
79
  <button type="button" role="button" class="secondary outline"
80
- hx-delete="/api/flock/htmx/agents/{{ agent.name }}"
80
+ hx-delete="/ui/api/flock/htmx/agents/{{ agent.name }}"
81
81
  hx-target="#agent-detail-form-content"
82
82
  hx-confirm="Are you sure you want to delete agent '{{ agent.name }}'?"
83
83
  hx-indicator="#agent-detail-loading-indicator">
@@ -6,7 +6,7 @@
6
6
  </div>
7
7
  {% else %}
8
8
  <div class="bubble bot">
9
- {{ entry.text }}
9
+ {{ entry.text | safe }}
10
10
  <span class="chat-timestamp">{{ entry.timestamp|default(now().strftime('%H:%M')) }}{% if entry.agent %} - {{ entry.agent }}{% endif %}{% if entry.duration_ms is defined %} - {{ entry.duration_ms }}ms{% endif %}</span>
11
11
  </div>
12
12
  {% endif %}
@@ -62,4 +62,26 @@
62
62
  <button type="button" class="secondary outline" hx-get="/chat/htmx/chat-view" hx-target="#chat-content-area" hx-swap="innerHTML">Cancel</button>
63
63
  </div>
64
64
  <div id="chat-settings-saving" class="htmx-indicator" style="text-align:center; margin-top:0.5rem;"><progress indeterminate></progress></div>
65
+
66
+ {# Share Chat Link Section #}
67
+ <div style="margin-top: 1.5rem; padding-top: 1rem; border-top: 1px solid var(--pico-muted-border-color);">
68
+ <h5 style="margin-bottom: 0.5rem;">Share Chat Configuration</h5>
69
+ <p><small>Create a shareable link for this chat configuration. The current Flock and these chat settings will be frozen for the shared session.</small></p>
70
+ <a href="#"
71
+ id="shareChatHtmxLink"
72
+ hx-post="/ui/htmx/share/chat/generate-link"
73
+ hx-target="#shareChatLinkDisplayArea"
74
+ hx-swap="innerHTML"
75
+ hx-indicator="#share-chat-loading-indicator"
76
+ hx-include="closest form" {# Includes all fields from the parent form #}
77
+ style="text-decoration: underline; cursor: pointer; color: var(--pico-primary);">
78
+ Create shareable chat link...
79
+ </a>
80
+ <span id="share-chat-loading-indicator" class="htmx-indicator" style="margin-left: 0.5rem;">
81
+ <progress indeterminate style="width: 100px;"></progress>
82
+ </span>
83
+ <div id="shareChatLinkDisplayArea" style="margin-top: 1rem;">
84
+ <!-- Shareable chat link will be loaded here by HTMX -->
85
+ </div>
86
+ </div>
65
87
  </form>
@@ -36,6 +36,20 @@
36
36
  </div>
37
37
 
38
38
  <button type="submit" {% if not flock.agents %}disabled{% endif %}>Run Flock</button>
39
+
40
+ <div id="share-agent-link-container" style="margin-top: 0.5rem;">
41
+ <a href="#"
42
+ id="shareAgentHtmxLink"
43
+ hx-post="/ui/htmx/share/generate-link"
44
+ hx-target="#shareLinkDisplayArea"
45
+ hx-swap="innerHTML"
46
+ hx-indicator="#share-loading-indicator"
47
+ hx-include="#start_agent_name_select"
48
+ style="text-decoration: underline; cursor: pointer; color: var(--pico-primary);">
49
+ Create shareable link...
50
+ </a>
51
+ </div>
52
+
39
53
  <span id="run-loading-indicator" class="htmx-indicator">
40
54
  <progress indeterminate></progress> Running...
41
55
  </span>
@@ -45,4 +59,17 @@
45
59
  {% else %}
46
60
  <p>Load or create a Flock to enable execution.</p>
47
61
  {% endif %}
48
- </article>
62
+
63
+ <div id="shareLinkDisplayArea" style="margin-top: 1rem;">
64
+ <!-- Content will be loaded here by HTMX -->
65
+ </div>
66
+
67
+ </article>
68
+
69
+ <script>
70
+ document.addEventListener('DOMContentLoaded', function() {
71
+ // All previous JavaScript for toggling share link visibility is removed.
72
+ // If there are other unrelated JavaScript functions in this script block,
73
+ // they would remain.
74
+ });
75
+ </script>
@@ -23,7 +23,7 @@
23
23
  </div>
24
24
  {% endif %}
25
25
 
26
- <form hx-post="/api/flock/htmx/flock-properties" hx-target="#flock-properties-form-article" hx-swap="innerHTML" hx-indicator="#flock-props-loading">
26
+ <form hx-post="/ui/api/flock/htmx/flock-properties" hx-target="#flock-properties-form-article" hx-swap="innerHTML" hx-indicator="#flock-props-loading">
27
27
  <label for="flock_name">Flock Name</label>
28
28
  <input type="text" id="flock_name" name="flock_name" value="{{ flock.name if flock else '' }}" required>
29
29
 
@@ -38,7 +38,7 @@
38
38
  </div>
39
39
  </form>
40
40
  <hr>
41
- <form hx-post="/api/flock/htmx/save-flock" hx-target="#flock-properties-form-article" hx-swap="innerHTML" hx-indicator="#flock-save-loading">
41
+ <form hx-post="/ui/api/flock/htmx/save-flock" hx-target="#flock-properties-form-article" hx-swap="innerHTML" hx-indicator="#flock-save-loading">
42
42
  <label for="save_filename">Save Flock As:</label>
43
43
  <input type="text" id="save_filename" name="save_filename"
44
44
  value="{{ current_filename if current_filename else (flock.name.replace(' ', '_').lower() + '.flock.yaml' if flock and flock.name else 'my_flock.flock.yaml') }}"
@@ -10,24 +10,28 @@
10
10
  </header>
11
11
 
12
12
  <div x-show="viewMode === 'json'">
13
- {% if result_data is string %}
14
- <p class="error" style="white-space: pre-wrap;">{{ result_data }}</p>
15
- {% elif result_data %}
16
- <pre><code style="word-break: break-all; white-space: pre-wrap;">{{ result_data | tojson(indent=2) }}</code></pre>
13
+ {# Check for the new result_raw_json variable first #}
14
+ {% if result_raw_json is defined %}
15
+ <pre><code class="language-json" style="word-break: break-all; white-space: pre-wrap;">{{ result_raw_json }}</code></pre>
16
+ {# Fallback for old context or if result was an error string from backend before formatting #}
17
+ {% elif result is string %}
18
+ <pre><code class="language-plaintext" style="word-break: break-all; white-space: pre-wrap;">{{ result }}</code></pre>
19
+ {% elif result %}
20
+ {# Fallback if result_raw_json is somehow not provided but result (dict) is #}
21
+ <pre><code class="language-json" style="word-break: break-all; white-space: pre-wrap;">{{ result | tojson(indent=2) }}</code></pre>
17
22
  {% else %}
18
23
  <p>No results to display yet.</p>
19
24
  {% endif %}
20
25
  </div>
21
26
 
22
27
  <div x-show="viewMode === 'structured'">
23
- {% if result_data is string %}
24
- <p class="error" style="white-space: pre-wrap;">{{ result_data }}</p>
25
- {% elif result_data is mapping %}
26
- {# Call our new macro for structured display #}
27
- {{ render_value(result_data) }}
28
- {% elif result_data %}
28
+ {% if result is string %}
29
+ <p class="error" style="white-space: pre-wrap;">{{ result }}</p>
30
+ {% elif result is mapping %}
31
+ {{ render_value(result) }}
32
+ {% elif result %}
29
33
  <p>Structured view not available for this result type (not a dictionary).</p>
30
- <pre><code>{{ result_data | tojson(indent=2) }}</code></pre>
34
+ <pre><code class="language-json" style="word-break: break-all; white-space: pre-wrap;">{{ result | tojson(indent=2) }}</code></pre>
31
35
  {% else %}
32
36
  <p>No results to display yet.</p>
33
37
  {% endif %}
@@ -0,0 +1,11 @@
1
+ {# This snippet is returned by the /ui/htmx/share/chat/generate-link endpoint #}
2
+ {% if share_url %}
3
+ <p style="margin-bottom: 0.5rem;"><strong>Shareable Chat Link generated:</strong></p>
4
+ <input type="text" id="generatedShareChatLinkInput" value="{{ share_url }}" readonly style="width: 100%; margin-bottom: 0.5rem;">
5
+ <p><small>You can select the link above and copy it (Ctrl+C or Cmd+C).</small></p>
6
+ <p><small>This link will start a chat session with the agent '{{ agent_name }}' from Flock '{{ flock_name }}' using the currently saved settings.</small></p>
7
+ {% elif error_message %}
8
+ <p style="color: var(--pico-form-invalid-color);">Error generating chat link: {{ error_message }}</p>
9
+ {% else %}
10
+ <p><em>Something went wrong, no chat link generated.</em></p>
11
+ {% endif %}