{"app":{"id":28,"summary":"AI Company assistant ","versions":[8863,8864,8865,8866,8875],"created_by":"tristan795","created_at":"2026-03-04T10:37:41.940Z","votes":0,"approved":true,"apps":["ai"],"app_type":"raw","external_embed_url":"https://app.windmill.dev/public/windmill-labs/928711b4b9ac223354f283212d5e7594","value":{"runnables":{"sendAiMessage":{"name":"Send message to company AI assistant","path":"hub/flows/76","type":"path","fields":{"message":{"type":"user","fieldType":"string"},"session_id":{"type":"user","value":"","fieldType":"string"}},"schema":{"type":"object","order":["message","session_id"],"required":["message"],"properties":{"message":{"type":"string","description":"User's message"},"session_id":{"type":"string","default":"","description":""}}},"runType":"flow"},"getSalesMetrics":{"name":"Get sales metrics","type":"inline","fields":{},"inlineScript":{"lock":"{\n  \"dependencies\": {}\n}\n//bun.lock\n<empty>","assets":[],"schema":{"type":"object","$schema":"https://json-schema.org/draft/2020-12/schema","required":[],"properties":{}},"content":"// Give sales metrics, and allow you to manipulate them on specific date ranges.\n// We use mock data to keep things simple for this example\n// But you can can easily create a script that gets this data from your own database\n\nexport async function main() {\n  const today = new Date();\n  const results = [];\n\n  // Sales values that create realistic patterns (weekends lower, some spikes for promotions)\n  const baseSales = [1200, 850, 720, 1150, 1180, 1220, 1190, 3850, 3620, 3290,\n                     1450, 1280, 890, 1100, 6420, 5890, 5240, 1520, 1380, 1050,\n                     920, 1180, 1240, 1210, 1290, 1350, 1180, 1220, 1340, 1410];\n\n  for (let i = 29; i >= 0; i--) {\n    const date = new Date(today);\n    date.setDate(today.getDate() - i);\n    const dateStr = date.toISOString().split('T')[0];\n    results.push({\n      date: dateStr,\n      sales: baseSales[29 - i]\n    });\n  }\n\n  return results;\n}\n","language":"bun"}},"getMarketingActivations":{"name":"Get marketing activations","type":"inline","fields":{},"inlineScript":{"lock":"{\n  \"dependencies\": {}\n}\n//bun.lock\n<empty>","assets":[],"schema":{"type":"object","$schema":"https://json-schema.org/draft/2020-12/schema","required":[],"properties":{}},"content":"// Returns a list of marketing activations (campaigns, ads, promotions) with their dates, budgets, and reach.\n// This allows the AI agent to correlate marketing efforts with sales performance and measure campaign impact.\n// We use mock data to keep things simple for this example\n// But you can easily create a script that gets this data from your marketing platform or CRM\n\nexport async function main() {\n  const today = new Date();\n\n  // Campaign templates with days offset from today (negative = days ago)\n  const campaigns = [\n    { daysAgo: 28, name: \"Monthly Newsletter\", type: \"email\", budget: 500, reach: 15000 },\n    { daysAgo: 25, name: \"Instagram Ads - New Collection\", type: \"social_media\", budget: 1200, reach: 45000 },\n    { daysAgo: 21, name: \"Google Search Ads\", type: \"paid_search\", budget: 800, reach: 8000 },\n    { daysAgo: 17, name: \"Influencer Partnership - @fashionista\", type: \"influencer\", budget: 2000, reach: 120000 },\n    { daysAgo: 14, name: \"Flash Sale Newsletter\", type: \"email\", budget: 300, reach: 18000 },\n    { daysAgo: 10, name: \"TikTok Video Campaign\", type: \"social_media\", budget: 1500, reach: 85000 },\n    { daysAgo: 6, name: \"Retargeting Ads - Facebook\", type: \"social_media\", budget: 600, reach: 12000 },\n    { daysAgo: 3, name: \"Weekend Promo Email\", type: \"email\", budget: 400, reach: 20000 },\n  ];\n\n  return campaigns.map(campaign => {\n    const date = new Date(today);\n    date.setDate(today.getDate() - campaign.daysAgo);\n    return {\n      date: date.toISOString().split('T')[0],\n      name: campaign.name,\n      type: campaign.type,\n      budget: campaign.budget,\n      reach: campaign.reach\n    };\n  });\n}\n","language":"bun"}}},"files":{"/App.tsx":"import React, { useState, useEffect, useRef } from 'react'\nimport { backend } from './wmill'\nimport './index.css'\n\ntype Message = { role: \"user\" | \"assistant\"; content: string; timestamp?: Date; toolsUsed?: string[] };\n\nconst SYSTEM_PROMPT = `You are Boris, the internal AI assistant for Acme Corporation. Your role is to help employees find information about company policies, HR questions, internal processes, and general company knowledge.\n\n## Company Overview\n- **Company Name**: Acme Corporation\n- **Founded**: 2015\n- **Headquarters**: 123 Innovation Drive, San Francisco, CA 94105\n- **CEO**: Sarah Johnson\n- **Number of Employees**: 450+\n\n## Key Contacts\n- **HR Director**: Maria Garcia (maria.garcia@acme.com)\n- **IT Support**: helpdesk@acme.com or ext. 5555\n- **Facilities Manager**: Tom Wilson (tom.wilson@acme.com)\n- **Finance Director**: David Chen (david.chen@acme.com)\n- **Legal Counsel**: Jennifer Smith (jennifer.smith@acme.com)\n\n## Payroll & Compensation\n- **Pay Schedule**: Bi-weekly, every other Friday\n- **Next Payday**: Check the HR portal for exact dates\n- **Direct Deposit**: Set up through the HR portal under \"Payroll Settings\"\n- **Pay Stubs**: Available on the HR portal 2 days before payday\n- **Annual Raises**: Performance reviews in March, raises effective April 1st\n- **Bonuses**: Annual bonuses paid in December based on company and individual performance\n\n## Time Off & Leave\n- **PTO Policy**: 20 days per year for all full-time employees (accrues monthly)\n- **Sick Leave**: 10 days per year (separate from PTO)\n- **How to Request**: Submit through the HR portal at least 2 weeks in advance for planned leave\n- **Parental Leave**: 16 weeks paid for primary caregivers, 8 weeks for secondary\n- **Bereavement**: 5 days for immediate family, 3 days for extended family\n- **Jury Duty**: Paid time off for the duration of service\n\n## Company Holidays (2024)\n1. New Year's Day - January 1\n2. Martin Luther King Jr. Day - January 15\n3. Presidents' Day - February 19\n4. Memorial Day - May 27\n5. Independence Day - July 4\n6. Labor Day - September 2\n7. Thanksgiving - November 28-29 (2 days)\n8. Christmas Eve & Day - December 24-25\n9. New Year's Eve - December 31\n\n## Benefits\n- **Health Insurance**: Blue Cross PPO and HMO options, company pays 80%\n- **Dental & Vision**: Included with health insurance\n- **401(k)**: Company matches 4% of salary\n- **Life Insurance**: 2x annual salary provided\n- **FSA/HSA**: Available for health and dependent care expenses\n- **Gym Membership**: $50/month reimbursement\n- **Professional Development**: $2,000/year for courses, conferences, certifications\n\n## Office Information\n- **Office Hours**: 8:00 AM - 6:00 PM, Monday to Friday\n- **Building Access**: Badge required, contact Security for issues\n- **Parking**: Free parking in Lot B, badge required for garage\n- **Kitchen**: Free coffee, tea, and snacks on each floor\n- **Meeting Rooms**: Book through Outlook calendar or the Rooms app\n\n## IT & Security\n- **Password Reset**: Self-service at password.acme.com or contact IT\n- **VPN Access**: Required for remote work, setup guide on IT portal\n- **Software Requests**: Submit ticket through IT portal\n- **Security Incidents**: Report immediately to security@acme.com\n\n## Remote Work Policy\n- **Hybrid Schedule**: 3 days in office (Tue, Wed, Thu), 2 days remote\n- **Equipment**: Laptop provided, $500 home office stipend for new hires\n- **Internet Reimbursement**: $50/month for remote work days\n\n## Expense Reimbursement\n- **Submit Through**: Concur expense system\n- **Deadline**: Within 30 days of expense\n- **Approval**: Manager approval required for expenses over $100\n- **Travel Policy**: Book through corporate travel portal for best rates\n\nBe helpful, friendly, and concise. If you don't know something specific, direct employees to the appropriate contact person or department.`;\n\nconst SUGGESTIONS = [\n  \"When is payday?\",\n  \"Who is in charge of HR?\",\n  \"How many sales did we make this week?\",\n  \"Which marketing campaign had the highest ROI?\",\n];\n\ntype Tool = {\n  id: string;\n  name: string;\n  description: string;\n  enabled: boolean;\n  backendMethod: string;\n};\n\nconst TOOLS: Tool[] = [\n  {\n    id: 'get_sale_metrics',\n    name: 'Sales Metrics',\n    description: 'Triggers a script that gets sales metrics from the last 30 days',\n    enabled: true,\n    backendMethod: 'getSalesMetrics',\n  },\n  {\n    id: 'get_marketing_activations',\n    name: 'Marketing Activations',\n    description: 'Triggers a script that gets marketing activations from the last 30 days',\n    enabled: true,\n    backendMethod: 'getMarketingActivations',\n  },\n];\n\nfunction generateSessionId(): string {\n  return crypto.randomUUID();\n}\n\nfunction getSessionId(): string {\n  let sessionId = sessionStorage.getItem('chat_session_id');\n  if (!sessionId) {\n    sessionId = generateSessionId();\n    sessionStorage.setItem('chat_session_id', sessionId);\n  }\n  return sessionId;\n}\n\nconst BotIcon = () => (\n  <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\">\n    <rect x=\"3\" y=\"11\" width=\"18\" height=\"10\" rx=\"2\" />\n    <circle cx=\"12\" cy=\"5\" r=\"2\" />\n    <path d=\"M12 7v4\" />\n    <line x1=\"8\" y1=\"16\" x2=\"8\" y2=\"16\" />\n    <line x1=\"16\" y1=\"16\" x2=\"16\" y2=\"16\" />\n  </svg>\n);\n\nconst UserIcon = () => (\n  <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\">\n    <path d=\"M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2\" />\n    <circle cx=\"12\" cy=\"7\" r=\"4\" />\n  </svg>\n);\n\nconst SendIcon = () => (\n  <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\">\n    <line x1=\"22\" y1=\"2\" x2=\"11\" y2=\"13\" />\n    <polygon points=\"22 2 15 22 11 13 2 9 22 2\" />\n  </svg>\n);\n\nconst ChatIcon = () => (\n  <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\">\n    <path d=\"M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z\" />\n  </svg>\n);\n\nconst DocIcon = () => (\n  <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\">\n    <path d=\"M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z\" />\n    <polyline points=\"14 2 14 8 20 8\" />\n    <line x1=\"16\" y1=\"13\" x2=\"8\" y2=\"13\" />\n    <line x1=\"16\" y1=\"17\" x2=\"8\" y2=\"17\" />\n    <polyline points=\"10 9 9 9 8 9\" />\n  </svg>\n);\n\nconst ToolsIcon = () => (\n  <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\">\n    <path d=\"M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z\" />\n  </svg>\n);\n\nconst CheckIcon = () => (\n  <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\">\n    <polyline points=\"20 6 9 17 4 12\" />\n  </svg>\n);\n\nconst PlayIcon = () => (\n  <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\">\n    <polygon points=\"5 3 19 12 5 21 5 3\" />\n  </svg>\n);\n\nconst CloseIcon = () => (\n  <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\">\n    <line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\" />\n    <line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\" />\n  </svg>\n);\n\nconst WrenchIcon = () => (\n  <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" width=\"12\" height=\"12\">\n    <path d=\"M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z\" />\n  </svg>\n);\n\ntype Tab = 'chat' | 'context' | 'tools';\n\nconst App = () => {\n  const [messages, setMessages] = useState<Message[]>([]);\n  const [input, setInput] = useState('');\n  const [loading, setLoading] = useState(false);\n  const [error, setError] = useState<string | null>(null);\n  const [sessionId, setSessionId] = useState<string>(getSessionId());\n  const [activeTab, setActiveTab] = useState<Tab>('chat');\n  const [toolResults, setToolResults] = useState<Record<string, any>>({});\n  const [toolLoading, setToolLoading] = useState<Record<string, boolean>>({});\n  const messagesContainerRef = useRef<HTMLDivElement>(null);\n  const inputRef = useRef<HTMLInputElement>(null);\n\n  const scrollToBottom = () => {\n    // Only scroll if there are messages and the container exists\n    if (messages.length > 0 && messagesContainerRef.current) {\n      // Use scrollTop on the container instead of scrollIntoView\n      // This prevents the parent page from scrolling when in an iframe\n      messagesContainerRef.current.scrollTop = messagesContainerRef.current.scrollHeight;\n    }\n  };\n\n  useEffect(() => {\n    scrollToBottom();\n  }, [messages]);\n\n  async function sendMessage(e: React.FormEvent) {\n    e.preventDefault();\n    if (!input.trim() || loading) return;\n\n    const userMessage = input.trim();\n    setInput('');\n    setError(null);\n    setLoading(true);\n\n    setMessages(prev => [...prev, { role: 'user', content: userMessage, timestamp: new Date() }]);\n\n    try {\n      const response = await backend.sendAiMessage({\n        session_id: sessionId,\n        message: userMessage,\n        system_prompt: SYSTEM_PROMPT\n      });\n      const content = typeof response === 'string' ? response : response.content;\n      // Extract tools used from response if available\n      const toolsUsed = response?.tool_calls as string[] | undefined;\n      setMessages(prev => [...prev, { role: 'assistant', content, timestamp: new Date(), toolsUsed }]);\n    } catch (e) {\n      console.error('Failed to send message:', e);\n      setError('Failed to send message. Please try again.');\n      setMessages(prev => prev.slice(0, -1));\n    }\n    setLoading(false);\n  }\n\n  function startNewChat() {\n    const newSessionId = generateSessionId();\n    sessionStorage.setItem('chat_session_id', newSessionId);\n    setSessionId(newSessionId);\n    setMessages([]);\n    setError(null);\n  }\n\n  async function testTool(tool: Tool) {\n    setToolLoading(prev => ({ ...prev, [tool.id]: true }));\n    setToolResults(prev => ({ ...prev, [tool.id]: undefined }));\n    try {\n      const backendFn = (backend as any)[tool.backendMethod];\n      if (!backendFn) {\n        throw new Error(`Backend method ${tool.backendMethod} not found`);\n      }\n      const result = await backendFn();\n      setToolResults(prev => ({ ...prev, [tool.id]: result }));\n    } catch (e) {\n      console.error('Failed to test tool:', e);\n      setToolResults(prev => ({ ...prev, [tool.id]: { error: String(e) } }));\n    }\n    setToolLoading(prev => ({ ...prev, [tool.id]: false }));\n  }\n\n  function clearToolResult(toolId: string) {\n    setToolResults(prev => ({ ...prev, [toolId]: undefined }));\n  }\n\n  return (\n    <div className=\"chat-app\">\n      <div className=\"chat-container\">\n        <header className=\"chat-header\">\n          <div className=\"header-left\">\n            <div className=\"bot-avatar\">\n              <BotIcon />\n            </div>\n            <div className=\"header-info\">\n              <h1>Boris</h1>\n              <span className=\"subtitle\">AI Assistant</span>\n            </div>\n          </div>\n          <div className=\"header-actions\">\n            {activeTab === 'chat' && (\n              <button onClick={startNewChat} className=\"new-chat-btn\">\n                New Chat\n              </button>\n            )}\n          </div>\n        </header>\n\n        <div className=\"tabs\">\n          <button\n            className={`tab ${activeTab === 'chat' ? 'active' : ''}`}\n            onClick={() => setActiveTab('chat')}\n          >\n            <ChatIcon />\n            Chat\n          </button>\n          <button\n            className={`tab ${activeTab === 'context' ? 'active' : ''}`}\n            onClick={() => setActiveTab('context')}\n          >\n            <DocIcon />\n            Knowledge Base\n          </button>\n          <button\n            className={`tab ${activeTab === 'tools' ? 'active' : ''}`}\n            onClick={() => setActiveTab('tools')}\n          >\n            <ToolsIcon />\n            Tools\n          </button>\n        </div>\n\n        {activeTab === 'chat' ? (\n          <>\n            <main className=\"messages-container\" ref={messagesContainerRef}>\n              {messages.length === 0 ? (\n                <div className=\"empty-state\">\n                  <div className=\"empty-icon\">\n                    <BotIcon />\n                  </div>\n                  <h2>Hi, I'm Boris!</h2>\n                  <p>Ask me anything about your company</p>\n                  <div className=\"suggestions\">\n                    {SUGGESTIONS.map((suggestion, idx) => (\n                      <button\n                        key={idx}\n                        className=\"suggestion-btn\"\n                        onClick={() => {\n                          setInput(suggestion);\n                          inputRef.current?.focus({ preventScroll: true });\n                        }}\n                      >\n                        {suggestion}\n                      </button>\n                    ))}\n                  </div>\n                </div>\n              ) : (\n                <div className=\"messages-list\">\n                  {messages.map((msg, idx) => (\n                    <div key={idx} className={`message-wrapper ${msg.role}`}>\n                      <div className=\"message-avatar\">\n                        {msg.role === 'assistant' ? <BotIcon /> : <UserIcon />}\n                      </div>\n                      <div className=\"message-bubble\">\n                        {msg.toolsUsed && msg.toolsUsed.length > 0 && (\n                          <div className=\"tools-used\">\n                            <WrenchIcon />\n                            <span className=\"tools-used-label\">Used:</span>\n                            {msg.toolsUsed.map((tool, i) => (\n                              <span key={i} className=\"tool-badge\">{tool.replace(/_/g, ' ')}</span>\n                            ))}\n                          </div>\n                        )}\n                        <div className=\"message-content\">{msg.content}</div>\n                      </div>\n                    </div>\n                  ))}\n                  {loading && (\n                    <div className=\"message-wrapper assistant\">\n                      <div className=\"message-avatar\">\n                        <BotIcon />\n                      </div>\n                      <div className=\"message-bubble\">\n                        <div className=\"typing-indicator\">\n                          <span></span>\n                          <span></span>\n                          <span></span>\n                        </div>\n                      </div>\n                    </div>\n                  )}\n                </div>\n              )}\n              {error && (\n                <div className=\"error-toast\">\n                  <span>{error}</span>\n                  <button onClick={() => setError(null)}>&times;</button>\n                </div>\n              )}\n            </main>\n\n            <footer className=\"input-container\">\n              <form onSubmit={sendMessage} className=\"input-form\">\n                <input\n                  ref={inputRef}\n                  type=\"text\"\n                  value={input}\n                  onChange={(e) => setInput(e.target.value)}\n                  placeholder=\"Type your message...\"\n                  disabled={loading}\n                  className=\"message-input\"\n                />\n                <button\n                  type=\"submit\"\n                  disabled={loading || !input.trim()}\n                  className=\"send-btn\"\n                  aria-label=\"Send message\"\n                >\n                  <SendIcon />\n                </button>\n              </form>\n            </footer>\n          </>\n        ) : activeTab === 'context' ? (\n          <main className=\"context-container\">\n            <div className=\"context-header\">\n              <h2>Knowledge Base</h2>\n              <p>This is the information Boris uses to answer your questions</p>\n            </div>\n            <div className=\"context-content\">\n              <pre>{SYSTEM_PROMPT}</pre>\n            </div>\n          </main>\n        ) : (\n          <main className=\"tools-container\">\n            <div className=\"tools-header\">\n              <h2>Available Tools</h2>\n              <p>These tools allow Boris to trigger scripts and flows from your workspace</p>\n            </div>\n            <div className=\"tools-list\">\n              {TOOLS.map((tool) => (\n                <div key={tool.id} className={`tool-card ${tool.enabled ? 'enabled' : 'disabled'}`}>\n                  <div className=\"tool-card-header\">\n                    <div className=\"tool-info\">\n                      <h3>{tool.name}</h3>\n                      <p>{tool.description}</p>\n                    </div>\n                    <div className=\"tool-actions\">\n                      {tool.enabled && (\n                        <button\n                          className=\"tool-test-btn\"\n                          onClick={() => testTool(tool)}\n                          disabled={toolLoading[tool.id]}\n                        >\n                          {toolLoading[tool.id] ? (\n                            <div className=\"loading-spinner small\"></div>\n                          ) : (\n                            <PlayIcon />\n                          )}\n                          Test\n                        </button>\n                      )}\n                      <div className={`tool-status ${tool.enabled ? 'enabled' : ''}`}>\n                        {tool.enabled && <CheckIcon />}\n                        {tool.enabled ? 'Enabled' : 'Coming soon'}\n                      </div>\n                    </div>\n                  </div>\n                  {toolResults[tool.id] !== undefined && (\n                    <div className=\"tool-result\">\n                      <div className=\"tool-result-header\">\n                        <span className=\"result-label\">Result:</span>\n                        <button className=\"close-result-btn\" onClick={() => clearToolResult(tool.id)}>\n                          <CloseIcon />\n                        </button>\n                      </div>\n                      <pre className=\"tool-result-json\">\n                        {JSON.stringify(toolResults[tool.id], null, 2)}\n                      </pre>\n                    </div>\n                  )}\n                </div>\n              ))}\n            </div>\n          </main>\n        )}\n      </div>\n    </div>\n  );\n};\n\nexport default App;\n","/index.css":"* {\n  box-sizing: border-box;\n  margin: 0;\n  padding: 0;\n}\n\n:root {\n  --primary: #18181b;\n  --primary-dark: #09090b;\n  --primary-light: #3f3f46;\n  --bg-main: #f8fafc;\n  --bg-chat: #ffffff;\n  --bg-user: #18181b;\n  --bg-assistant: #f1f5f9;\n  --text-primary: #1e293b;\n  --text-secondary: #64748b;\n  --text-light: #94a3b8;\n  --border: #e2e8f0;\n  --success: #22c55e;\n  --error: #ef4444;\n  --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);\n  --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1);\n  --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1);\n  --radius-sm: 8px;\n  --radius-md: 12px;\n  --radius-lg: 16px;\n  --radius-full: 9999px;\n}\n\nbody {\n  font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n  background: var(--bg-main);\n  color: var(--text-primary);\n  line-height: 1.5;\n  -webkit-font-smoothing: antialiased;\n}\n\n.chat-app {\n  min-height: 100vh;\n  display: flex;\n  align-items: stretch;\n  justify-content: stretch;\n}\n\n.chat-container {\n  width: 100%;\n  height: 100vh;\n  background: var(--bg-chat);\n  display: flex;\n  flex-direction: column;\n  overflow: hidden;\n}\n\n/* Header */\n.chat-header {\n  display: flex;\n  justify-content: space-between;\n  align-items: center;\n  padding: 16px 20px;\n  background: var(--bg-chat);\n  border-bottom: 1px solid var(--border);\n}\n\n.header-left {\n  display: flex;\n  align-items: center;\n  gap: 12px;\n}\n\n.bot-avatar {\n  width: 44px;\n  height: 44px;\n  background: linear-gradient(135deg, var(--primary), var(--primary-dark));\n  border-radius: var(--radius-md);\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  color: white;\n}\n\n.bot-avatar svg {\n  width: 24px;\n  height: 24px;\n}\n\n.header-info h1 {\n  font-size: 1rem;\n  font-weight: 600;\n  color: var(--text-primary);\n}\n\n.subtitle {\n  font-size: 0.8rem;\n  color: var(--text-secondary);\n}\n\n.header-actions {\n  display: flex;\n  align-items: center;\n  gap: 8px;\n}\n\n.new-chat-btn {\n  padding: 8px 16px;\n  background: var(--bg-assistant);\n  color: var(--text-primary);\n  border: 1px solid var(--border);\n  border-radius: var(--radius-sm);\n  font-size: 0.875rem;\n  font-weight: 500;\n  cursor: pointer;\n  transition: all 0.2s;\n}\n\n.new-chat-btn:hover {\n  background: var(--border);\n}\n\n/* Tabs */\n.tabs {\n  display: flex;\n  background: var(--bg-chat);\n  border-bottom: 1px solid var(--border);\n  padding: 0 20px;\n}\n\n.tab {\n  display: flex;\n  align-items: center;\n  gap: 8px;\n  padding: 12px 20px;\n  background: none;\n  border: none;\n  border-bottom: 2px solid transparent;\n  color: var(--text-secondary);\n  font-size: 0.875rem;\n  font-weight: 500;\n  cursor: pointer;\n  transition: all 0.2s;\n  margin-bottom: -1px;\n}\n\n.tab:hover {\n  color: var(--text-primary);\n}\n\n.tab.active {\n  color: var(--primary);\n  border-bottom-color: var(--primary);\n}\n\n.tab svg {\n  width: 18px;\n  height: 18px;\n}\n\n/* Messages Container */\n.messages-container {\n  flex: 1;\n  overflow-y: auto;\n  padding: 20px;\n  background: var(--bg-main);\n  position: relative;\n}\n\n.messages-list {\n  display: flex;\n  flex-direction: column;\n  gap: 16px;\n}\n\n/* Context Container */\n.context-container {\n  flex: 1;\n  overflow-y: auto;\n  padding: 24px;\n  background: var(--bg-main);\n}\n\n.context-header {\n  margin-bottom: 20px;\n}\n\n.context-header h2 {\n  font-size: 1.25rem;\n  font-weight: 600;\n  color: var(--text-primary);\n  margin-bottom: 4px;\n}\n\n.context-header p {\n  font-size: 0.875rem;\n  color: var(--text-secondary);\n}\n\n.context-content {\n  background: var(--bg-chat);\n  border: 1px solid var(--border);\n  border-radius: var(--radius-md);\n  padding: 20px;\n  overflow-x: auto;\n}\n\n.context-content pre {\n  font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', monospace;\n  font-size: 0.8rem;\n  line-height: 1.7;\n  color: var(--text-primary);\n  white-space: pre-wrap;\n  word-wrap: break-word;\n}\n\n/* Tools Container */\n.tools-container {\n  flex: 1;\n  overflow-y: auto;\n  padding: 24px;\n  background: var(--bg-main);\n}\n\n.tools-header {\n  margin-bottom: 24px;\n}\n\n.tools-header h2 {\n  font-size: 1.25rem;\n  font-weight: 600;\n  color: var(--text-primary);\n  margin-bottom: 4px;\n}\n\n.tools-header p {\n  font-size: 0.875rem;\n  color: var(--text-secondary);\n}\n\n.tools-list {\n  display: flex;\n  flex-direction: column;\n  gap: 16px;\n}\n\n.tool-card {\n  background: var(--bg-chat);\n  border: 1px solid var(--border);\n  border-radius: var(--radius-md);\n  padding: 20px;\n  transition: all 0.2s;\n}\n\n.tool-card.enabled {\n}\n\n.tool-card.disabled {\n  opacity: 0.7;\n}\n\n.tool-card-header {\n  display: flex;\n  justify-content: space-between;\n  align-items: center;\n  gap: 16px;\n}\n\n.tool-actions {\n  display: flex;\n  align-items: center;\n  gap: 12px;\n}\n\n.tool-test-btn {\n  display: flex;\n  align-items: center;\n  gap: 6px;\n  padding: 8px 14px;\n  background: var(--primary);\n  color: white;\n  border: none;\n  border-radius: var(--radius-sm);\n  font-size: 0.8rem;\n  font-weight: 500;\n  cursor: pointer;\n  transition: all 0.2s;\n}\n\n.tool-test-btn:hover:not(:disabled) {\n  background: var(--primary-dark);\n}\n\n.tool-test-btn:disabled {\n  opacity: 0.7;\n  cursor: not-allowed;\n}\n\n.tool-test-btn svg {\n  width: 14px;\n  height: 14px;\n}\n\n.tool-test-btn .loading-spinner.small {\n  width: 14px;\n  height: 14px;\n  border-width: 2px;\n}\n\n.tool-info h3 {\n  font-size: 1rem;\n  font-weight: 600;\n  color: var(--text-primary);\n  margin-bottom: 4px;\n}\n\n.tool-info p {\n  font-size: 0.875rem;\n  color: var(--text-secondary);\n}\n\n.tool-status {\n  display: flex;\n  align-items: center;\n  gap: 6px;\n  padding: 6px 12px;\n  background: var(--bg-assistant);\n  border-radius: var(--radius-full);\n  font-size: 0.75rem;\n  font-weight: 500;\n  color: var(--text-secondary);\n  white-space: nowrap;\n}\n\n.tool-status.enabled {\n  background: rgba(34, 197, 94, 0.1);\n  color: var(--success);\n}\n\n.tool-status svg {\n  width: 14px;\n  height: 14px;\n}\n\n.tool-result {\n  margin-top: 16px;\n  border-top: 1px solid var(--border);\n  padding-top: 16px;\n}\n\n.tool-result-header {\n  display: flex;\n  justify-content: space-between;\n  align-items: center;\n  margin-bottom: 12px;\n}\n\n.result-label {\n  font-size: 0.75rem;\n  font-weight: 500;\n  color: var(--text-light);\n  text-transform: uppercase;\n  letter-spacing: 0.05em;\n}\n\n.close-result-btn {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  width: 24px;\n  height: 24px;\n  background: var(--bg-assistant);\n  border: none;\n  border-radius: var(--radius-sm);\n  color: var(--text-secondary);\n  cursor: pointer;\n  transition: all 0.2s;\n}\n\n.close-result-btn:hover {\n  background: var(--border);\n  color: var(--text-primary);\n}\n\n.close-result-btn svg {\n  width: 14px;\n  height: 14px;\n}\n\n.tool-result-json {\n  background: var(--bg-main);\n  border: 1px solid var(--border);\n  border-radius: var(--radius-sm);\n  padding: 16px;\n  font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', monospace;\n  font-size: 0.75rem;\n  line-height: 1.6;\n  color: var(--text-primary);\n  overflow-x: auto;\n  max-height: 300px;\n  overflow-y: auto;\n  white-space: pre-wrap;\n  word-wrap: break-word;\n}\n\n/* Loading State */\n.loading-container {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  justify-content: center;\n  height: 100%;\n  gap: 16px;\n  color: var(--text-secondary);\n}\n\n.loading-spinner {\n  width: 40px;\n  height: 40px;\n  border: 3px solid var(--border);\n  border-top-color: var(--primary);\n  border-radius: 50%;\n  animation: spin 0.8s linear infinite;\n}\n\n@keyframes spin {\n  to { transform: rotate(360deg); }\n}\n\n/* Empty State */\n.empty-state {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  justify-content: center;\n  height: 100%;\n  text-align: center;\n  color: var(--text-secondary);\n}\n\n.empty-icon {\n  width: 80px;\n  height: 80px;\n  background: linear-gradient(135deg, var(--primary-light), var(--primary));\n  border-radius: var(--radius-lg);\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  color: white;\n  margin-bottom: 20px;\n}\n\n.empty-icon svg {\n  width: 40px;\n  height: 40px;\n}\n\n.empty-state h2 {\n  font-size: 1.25rem;\n  font-weight: 600;\n  color: var(--text-primary);\n  margin-bottom: 8px;\n}\n\n.empty-state p {\n  font-size: 0.95rem;\n}\n\n.suggestions {\n  display: flex;\n  flex-wrap: wrap;\n  gap: 8px;\n  margin-top: 24px;\n  justify-content: center;\n  max-width: 500px;\n}\n\n.suggestion-btn {\n  padding: 10px 16px;\n  background: var(--bg-chat);\n  color: var(--text-primary);\n  border: 1px solid var(--border);\n  border-radius: var(--radius-full);\n  font-size: 0.875rem;\n  cursor: pointer;\n  transition: all 0.2s;\n}\n\n.suggestion-btn:hover {\n  background: var(--primary);\n  color: white;\n  border-color: var(--primary);\n}\n\n/* Message Wrapper */\n.message-wrapper {\n  display: flex;\n  gap: 12px;\n  max-width: 85%;\n  animation: fadeIn 0.3s ease;\n}\n\n@keyframes fadeIn {\n  from {\n    opacity: 0;\n    transform: translateY(10px);\n  }\n  to {\n    opacity: 1;\n    transform: translateY(0);\n  }\n}\n\n.message-wrapper.user {\n  flex-direction: row-reverse;\n  margin-left: auto;\n}\n\n.message-avatar {\n  width: 36px;\n  height: 36px;\n  border-radius: var(--radius-sm);\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  flex-shrink: 0;\n}\n\n.message-wrapper.assistant .message-avatar {\n  background: linear-gradient(135deg, var(--primary), var(--primary-dark));\n  color: white;\n}\n\n.message-wrapper.user .message-avatar {\n  background: var(--bg-assistant);\n  color: var(--text-secondary);\n}\n\n.message-avatar svg {\n  width: 20px;\n  height: 20px;\n}\n\n/* Message Bubble */\n.message-bubble {\n  padding: 12px 16px;\n  border-radius: var(--radius-md);\n  line-height: 1.5;\n}\n\n.message-wrapper.assistant .message-bubble {\n  background: var(--bg-chat);\n  color: var(--text-primary);\n  border: 1px solid var(--border);\n  border-bottom-left-radius: 4px;\n}\n\n.message-wrapper.user .message-bubble {\n  background: var(--bg-user);\n  color: white;\n  border-bottom-right-radius: 4px;\n}\n\n.message-content {\n  font-size: 0.925rem;\n  white-space: pre-wrap;\n  word-wrap: break-word;\n}\n\n/* Tools Used in Message */\n.tools-used {\n  display: flex;\n  align-items: center;\n  gap: 6px;\n  flex-wrap: wrap;\n  margin-bottom: 10px;\n  padding-bottom: 10px;\n  border-bottom: 1px solid var(--border);\n}\n\n.tools-used svg {\n  color: var(--text-secondary);\n  flex-shrink: 0;\n}\n\n.tools-used-label {\n  font-size: 0.75rem;\n  color: var(--text-secondary);\n  font-weight: 500;\n}\n\n.tool-badge {\n  display: inline-flex;\n  align-items: center;\n  padding: 3px 10px;\n  background: linear-gradient(135deg, rgba(24, 24, 27, 0.08), rgba(24, 24, 27, 0.12));\n  border: 1px solid var(--border);\n  border-radius: var(--radius-full);\n  font-size: 0.7rem;\n  font-weight: 500;\n  color: var(--primary);\n  text-transform: capitalize;\n}\n\n/* Typing Indicator */\n.typing-indicator {\n  display: flex;\n  gap: 4px;\n  padding: 4px 0;\n}\n\n.typing-indicator span {\n  width: 8px;\n  height: 8px;\n  background: var(--text-light);\n  border-radius: 50%;\n  animation: bounce 1.4s infinite ease-in-out;\n}\n\n.typing-indicator span:nth-child(1) { animation-delay: 0s; }\n.typing-indicator span:nth-child(2) { animation-delay: 0.2s; }\n.typing-indicator span:nth-child(3) { animation-delay: 0.4s; }\n\n@keyframes bounce {\n  0%, 80%, 100% {\n    transform: scale(0.8);\n    opacity: 0.5;\n  }\n  40% {\n    transform: scale(1);\n    opacity: 1;\n  }\n}\n\n/* Error Toast */\n.error-toast {\n  position: absolute;\n  bottom: 20px;\n  left: 50%;\n  transform: translateX(-50%);\n  background: var(--error);\n  color: white;\n  padding: 12px 16px;\n  border-radius: var(--radius-sm);\n  display: flex;\n  align-items: center;\n  gap: 12px;\n  font-size: 0.875rem;\n  box-shadow: var(--shadow-md);\n  animation: slideUp 0.3s ease;\n}\n\n@keyframes slideUp {\n  from {\n    opacity: 0;\n    transform: translate(-50%, 10px);\n  }\n  to {\n    opacity: 1;\n    transform: translate(-50%, 0);\n  }\n}\n\n.error-toast button {\n  background: none;\n  border: none;\n  color: white;\n  font-size: 1.25rem;\n  cursor: pointer;\n  opacity: 0.8;\n  transition: opacity 0.2s;\n}\n\n.error-toast button:hover {\n  opacity: 1;\n}\n\n/* Input Container */\n.input-container {\n  padding: 16px 20px;\n  background: var(--bg-chat);\n  border-top: 1px solid var(--border);\n}\n\n.input-form {\n  display: flex;\n  gap: 12px;\n  align-items: center;\n}\n\n.message-input {\n  flex: 1;\n  padding: 14px 18px;\n  border: 1px solid var(--border);\n  border-radius: var(--radius-full);\n  font-size: 0.925rem;\n  outline: none;\n  transition: all 0.2s;\n  background: var(--bg-main);\n}\n\n.message-input:focus {\n  border-color: var(--primary);\n  box-shadow: 0 0 0 3px rgba(24, 24, 27, 0.1);\n  background: var(--bg-chat);\n}\n\n.message-input:disabled {\n  background: var(--bg-assistant);\n  cursor: not-allowed;\n}\n\n.message-input::placeholder {\n  color: var(--text-light);\n}\n\n.send-btn {\n  width: 48px;\n  height: 48px;\n  background: var(--primary);\n  color: white;\n  border: none;\n  border-radius: 50%;\n  cursor: pointer;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  transition: all 0.2s;\n  flex-shrink: 0;\n}\n\n.send-btn:hover:not(:disabled) {\n  background: var(--primary-dark);\n  transform: scale(1.05);\n}\n\n.send-btn:disabled {\n  background: var(--text-light);\n  cursor: not-allowed;\n}\n\n.send-btn svg {\n  width: 20px;\n  height: 20px;\n}\n\n/* Scrollbar */\n.messages-container::-webkit-scrollbar,\n.context-container::-webkit-scrollbar,\n.tools-container::-webkit-scrollbar {\n  width: 6px;\n}\n\n.messages-container::-webkit-scrollbar-track,\n.context-container::-webkit-scrollbar-track,\n.tools-container::-webkit-scrollbar-track {\n  background: transparent;\n}\n\n.messages-container::-webkit-scrollbar-thumb,\n.context-container::-webkit-scrollbar-thumb,\n.tools-container::-webkit-scrollbar-thumb {\n  background: var(--border);\n  border-radius: 3px;\n}\n\n.messages-container::-webkit-scrollbar-thumb:hover,\n.context-container::-webkit-scrollbar-thumb:hover,\n.tools-container::-webkit-scrollbar-thumb:hover {\n  background: var(--text-light);\n}\n\n/* Responsive */\n@media (max-width: 640px) {\n  .message-wrapper {\n    max-width: 90%;\n  }\n\n  .tab {\n    padding: 12px 14px;\n    font-size: 0.8rem;\n  }\n\n  .tab svg {\n    width: 16px;\n    height: 16px;\n  }\n}\n","/index.tsx":"import React from 'react'\n\nimport { createRoot } from 'react-dom/client'\nimport App from './App'\n\nconst root = createRoot(document.getElementById('root')!);\nroot.render(<App/>);\n","/package.json":"{\n    \"dependencies\": {\n        \"react\": \"19.0.0\",\n        \"react-dom\": \"19.0.0\",\n        \"windmill-client\": \"^1\"\n    },\n    \"devDependencies\": {\n        \"@types/react-dom\": \"^19.0.0\",\n        \"@types/react\": \"^19.0.0\"\n    }\n}\n"},"data":{"tables":[]}},"description":"AI agent that answers questions about your company, using its knowledge base and real time data Windmill scripts used as tools. To make it work in your own workspace, you will also need to import the following flow: https://hub.windmill.dev/flows/76/send-message-to-company-ai-assistant","vcreated_at":"2026-04-14T14:55:05.310Z","vcreated_by":"hugo989","comments":[]}}