feat(agent): integrate agents into all BOC modules
BOC CI/CD / Test (push) Failing after 1s
BOC CI/CD / Security Scan (push) Has been skipped
BOC CI/CD / Build & Push (push) Has been skipped
BOC CI/CD / Deploy to Staging (push) Has been skipped
BOC CI/CD / Deploy to Production (push) Has been skipped

- Add AgentFAB floating button to CRM, Sales, Finance, HR, Legal, Marketing, Support, Automation
- Update AgentChat to use /api/v1/agents/chat with auth token
- Add error handling and fallback to mock responses
- Update Anthropic model to claude-sonnet-4-6
- Show agent context in chat footer (title + competencies)
- Build fresh web-v2 dist
This commit is contained in:
Bernt
2026-08-12 09:04:42 +00:00
parent 0831a49c71
commit 0124fc0186
16 changed files with 923 additions and 793 deletions
+1 -1
View File
@@ -120,7 +120,7 @@ func (o *AgentOrchestrator) callAnthropic(systemPrompt string, meddelanden []Age
} }
payload := map[string]interface{}{ payload := map[string]interface{}{
"model": "claude-3-sonnet-20240229", "model": "claude-sonnet-4-6",
"max_tokens": 1024, "max_tokens": 1024,
"system": systemPrompt, "system": systemPrompt,
"messages": messages, "messages": messages,
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -8,8 +8,8 @@
<link rel="preconnect" href="https://fonts.googleapis.com" /> <link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet" /> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet" />
<script type="module" crossorigin src="/assets/index-DGHqwpHs.js"></script> <script type="module" crossorigin src="/assets/index-CZzJtMVa.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-Bj_ETaak.css"> <link rel="stylesheet" crossorigin href="/assets/index-C4-IO3Fe.css">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
+39 -8
View File
@@ -1,7 +1,8 @@
import { useState, useRef, useEffect } from 'react'; import { useState, useRef, useEffect } from 'react';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { Send, Bot, User, X, Minimize2, Maximize2, Sparkles } from 'lucide-react'; import { Send, Bot, User, X, Minimize2, Maximize2, Sparkles, RefreshCw } from 'lucide-react';
import { getAgentContext, AgentContext } from './AgentContext'; import { getAgentContext, AgentContext } from './AgentContext';
import { useAuthStore } from '@/stores/authStore';
interface Meddelande { interface Meddelande {
id: string; id: string;
@@ -18,17 +19,19 @@ interface AgentChatProps {
export function AgentChat({ rum, isOpen, onToggle }: AgentChatProps) { export function AgentChat({ rum, isOpen, onToggle }: AgentChatProps) {
const context = getAgentContext(rum); const context = getAgentContext(rum);
const { token } = useAuthStore();
const [meddelanden, setMeddelanden] = useState<Meddelande[]>([ const [meddelanden, setMeddelanden] = useState<Meddelande[]>([
{ {
id: 'welcome', id: 'welcome',
roll: 'assistant', roll: 'assistant',
innehall: `Hej! Jag är ${context.titel}. Jag kan hjälpa dig med ${context.kompetenser.join(', ')}. Vad kan jag göra för dig?`, innehall: `Hej! Jag är ${context.titel}. ${context.systemPrompt.split('\n')[2] || 'Jag kan hjälpa dig med ' + context.kompetenser.join(', ') + '.'}\n\nVad kan jag göra för dig?`,
tid: new Date().toISOString() tid: new Date().toISOString()
} }
]); ]);
const [input, setInput] = useState(''); const [input, setInput] = useState('');
const [laddar, setLaddar] = useState(false); const [laddar, setLaddar] = useState(false);
const [isExpanded, setIsExpanded] = useState(false); const [isExpanded, setIsExpanded] = useState(false);
const [error, setError] = useState('');
const scrollRef = useRef<HTMLDivElement>(null); const scrollRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLTextAreaElement>(null); const inputRef = useRef<HTMLTextAreaElement>(null);
@@ -52,11 +55,16 @@ export function AgentChat({ rum, isOpen, onToggle }: AgentChatProps) {
setInput(''); setInput('');
setLaddar(true); setLaddar(true);
setError('');
try { try {
// Anropa Claude via OpenClaw API // Anropa BOC Agent API med auth
const response = await fetch('/api/agent/chat', { const response = await fetch('/api/v1/agents/chat', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: {
'Content-Type': 'application/json',
'Authorization': token ? `Bearer ${token}` : ''
},
body: JSON.stringify({ body: JSON.stringify({
rum: context.rum, rum: context.rum,
systemPrompt: context.systemPrompt, systemPrompt: context.systemPrompt,
@@ -75,7 +83,19 @@ export function AgentChat({ rum, isOpen, onToggle }: AgentChatProps) {
innehall: data.svar, innehall: data.svar,
tid: new Date().toISOString() tid: new Date().toISOString()
}]); }]);
} else if (response.status === 401) {
setError('Du måste logga in för att använda agenten');
// Fallback: Mock-svar
const mockSvar = genereraMockSvar(context, input.trim());
setMeddelanden(prev => [...prev, {
id: `assistant-${Date.now()}`,
roll: 'assistant',
innehall: mockSvar,
tid: new Date().toISOString()
}]);
} else { } else {
const errData = await response.json().catch(() => ({}));
setError(errData.error || 'Agenten svarade inte');
// Fallback: Mock-svar // Fallback: Mock-svar
const mockSvar = genereraMockSvar(context, input.trim()); const mockSvar = genereraMockSvar(context, input.trim());
setMeddelanden(prev => [...prev, { setMeddelanden(prev => [...prev, {
@@ -85,7 +105,8 @@ export function AgentChat({ rum, isOpen, onToggle }: AgentChatProps) {
tid: new Date().toISOString() tid: new Date().toISOString()
}]); }]);
} }
} catch (error) { } catch (err) {
setError('Kunde inte nå agenten. Använder lokalt svar.');
const mockSvar = genereraMockSvar(context, input.trim()); const mockSvar = genereraMockSvar(context, input.trim());
setMeddelanden(prev => [...prev, { setMeddelanden(prev => [...prev, {
id: `assistant-${Date.now()}`, id: `assistant-${Date.now()}`,
@@ -202,6 +223,16 @@ export function AgentChat({ rum, isOpen, onToggle }: AgentChatProps) {
)} )}
</div> </div>
{/* Error */}
{error && (
<div className="px-4 py-2 bg-danger/10 border-t border-danger/20">
<p className="text-xs text-danger flex items-center gap-1.5">
<RefreshCw size={12} />
{error}
</p>
</div>
)}
{/* Input */} {/* Input */}
<div className="p-4 border-t border-border"> <div className="p-4 border-t border-border">
<div className="flex gap-2"> <div className="flex gap-2">
@@ -210,7 +241,7 @@ export function AgentChat({ rum, isOpen, onToggle }: AgentChatProps) {
value={input} value={input}
onChange={(e) => setInput(e.target.value)} onChange={(e) => setInput(e.target.value)}
onKeyDown={hanteraKeyDown} onKeyDown={hanteraKeyDown}
placeholder="Skriv ett meddelande..." placeholder={`Fråga ${context.titel}...`}
className="flex-1 min-h-[44px] max-h-[120px] px-4 py-2.5 rounded-xl border bg-surface text-sm resize-none focus:outline-none focus:ring-2 focus:ring-primary/20" className="flex-1 min-h-[44px] max-h-[120px] px-4 py-2.5 rounded-xl border bg-surface text-sm resize-none focus:outline-none focus:ring-2 focus:ring-primary/20"
rows={1} rows={1}
/> />
@@ -223,7 +254,7 @@ export function AgentChat({ rum, isOpen, onToggle }: AgentChatProps) {
</button> </button>
</div> </div>
<p className="text-xs text-text-secondary mt-2 text-center"> <p className="text-xs text-text-secondary mt-2 text-center">
AI kan göra misstag. Verifiera viktig information. {context.titel} {context.kompetenser.slice(0, 3).join(' • ')}
</p> </p>
</div> </div>
</motion.div> </motion.div>
+59
View File
@@ -0,0 +1,59 @@
import { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { Bot, X } from 'lucide-react';
import { AgentChat } from './AgentChat';
import { cn } from '@/lib/utils';
interface AgentFABProps {
rum: string;
className?: string;
}
export function AgentFAB({ rum, className }: AgentFABProps) {
const [isOpen, setIsOpen] = useState(false);
return (
<>
<AnimatePresence>
{isOpen && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 bg-black/20 z-40"
onClick={() => setIsOpen(false)}
/>
)}
</AnimatePresence>
<AgentChat
rum={rum}
isOpen={isOpen}
onToggle={() => setIsOpen(!isOpen)}
/>
<motion.button
initial={{ scale: 0 }}
animate={{ scale: 1 }}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
onClick={() => setIsOpen(!isOpen)}
className={cn(
'fixed bottom-6 right-6 z-50 w-14 h-14 rounded-full shadow-lg shadow-primary/20',
'flex items-center justify-center transition-colors',
isOpen
? 'bg-danger text-white hover:bg-danger/90'
: 'bg-primary text-white hover:bg-primary/90',
className
)}
>
<motion.div
animate={{ rotate: isOpen ? 90 : 0 }}
transition={{ duration: 0.2 }}
>
{isOpen ? <X size={22} /> : <Bot size={22} />}
</motion.div>
</motion.button>
</>
);
}
+4
View File
@@ -13,6 +13,7 @@ import {
FileText, FileText,
UserPlus, UserPlus,
} from 'lucide-react' } from 'lucide-react'
import { AgentFAB } from '@/components/agent/AgentFAB'
const workflows = [ const workflows = [
{ {
@@ -202,6 +203,9 @@ export function AutomationPage() {
))} ))}
</div> </div>
</Card> </Card>
{/* Automation Agent */}
<AgentFAB rum="automation" />
</div> </div>
) )
} }
+4
View File
@@ -23,6 +23,7 @@ import {
Building2, Building2,
MoreHorizontal, MoreHorizontal,
} from 'lucide-react' } from 'lucide-react'
import { AgentFAB } from '@/components/agent/AgentFAB'
interface Customer { interface Customer {
id: string id: string
@@ -346,6 +347,9 @@ export function CRMPage() {
</Card> </Card>
)} )}
{/* CRM Agent */}
<AgentFAB rum="crm" />
{activeTab === 'Pipeline' && ( {activeTab === 'Pipeline' && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-5"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-5">
{pipelineStages.length === 0 && ( {pipelineStages.length === 0 && (
+4
View File
@@ -31,6 +31,7 @@ import {
ArrowUpRight, ArrowUpRight,
ArrowDownRight, ArrowDownRight,
} from 'lucide-react' } from 'lucide-react'
import { AgentFAB } from '@/components/agent/AgentFAB'
interface BalanceAccount { interface BalanceAccount {
account: string account: string
@@ -575,6 +576,9 @@ export function FinancePage() {
</div> </div>
</Card> </Card>
)} )}
{/* Finance Agent */}
<AgentFAB rum="finance" />
</div> </div>
) )
} }
+4
View File
@@ -22,6 +22,7 @@ import {
MoreHorizontal, MoreHorizontal,
Clock, Clock,
} from 'lucide-react' } from 'lucide-react'
import { AgentFAB } from '@/components/agent/AgentFAB'
interface Employee { interface Employee {
id: string id: string
@@ -347,6 +348,9 @@ export function HRPage() {
</Table> </Table>
</Card> </Card>
)} )}
{/* HR Agent */}
<AgentFAB rum="hr" />
</div> </div>
) )
} }
+4
View File
@@ -19,6 +19,7 @@ import {
Briefcase, Briefcase,
Globe, Globe,
} from 'lucide-react' } from 'lucide-react'
import { AgentFAB } from '@/components/agent/AgentFAB'
interface Contract { interface Contract {
id: string id: string
@@ -557,6 +558,9 @@ export function LegalPage() {
</div> </div>
</div> </div>
)} )}
{/* Legal Agent */}
<AgentFAB rum="legal" />
</div> </div>
) )
} }
+4
View File
@@ -10,6 +10,7 @@ import {
TrendingUp, TrendingUp,
Users, Users,
} from 'lucide-react' } from 'lucide-react'
import { AgentFAB } from '@/components/agent/AgentFAB'
const campaigns = [ const campaigns = [
{ id: '1', name: 'Q4 Product Launch', status: 'active' as const, channel: 'Email', reach: 12500, engagement: 8.2, conversions: 340 }, { id: '1', name: 'Q4 Product Launch', status: 'active' as const, channel: 'Email', reach: 12500, engagement: 8.2, conversions: 340 },
@@ -134,6 +135,9 @@ export function MarketingPage() {
))} ))}
</div> </div>
</Card> </Card>
{/* Marketing Agent */}
<AgentFAB rum="marketing" />
</div> </div>
) )
} }
+4
View File
@@ -33,6 +33,7 @@ import {
Filter, Filter,
MoreHorizontal, MoreHorizontal,
} from 'lucide-react' } from 'lucide-react'
import { AgentFAB } from '@/components/agent/AgentFAB'
interface Deal { interface Deal {
id: string id: string
@@ -402,6 +403,9 @@ export function SalesPage() {
)} )}
</Card> </Card>
)} )}
{/* Sales Agent */}
<AgentFAB rum="sales" />
</div> </div>
) )
} }
+4
View File
@@ -21,6 +21,7 @@ import {
Filter, Filter,
MoreHorizontal, MoreHorizontal,
} from 'lucide-react' } from 'lucide-react'
import { AgentFAB } from '@/components/agent/AgentFAB'
const tickets = [ const tickets = [
{ id: 'SUP-2024-001', subject: 'Login issues after password reset', customer: 'Acme Corp', priority: 'high' as const, status: 'open' as const, assigned: 'Johan Berg', created: new Date(Date.now() - 1000 * 60 * 30).toISOString() }, { id: 'SUP-2024-001', subject: 'Login issues after password reset', customer: 'Acme Corp', priority: 'high' as const, status: 'open' as const, assigned: 'Johan Berg', created: new Date(Date.now() - 1000 * 60 * 30).toISOString() },
@@ -212,6 +213,9 @@ export function SupportPage() {
</TableBody> </TableBody>
</Table> </Table>
</Card> </Card>
{/* Support Agent */}
<AgentFAB rum="support" />
</div> </div>
) )
} }