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
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.gstatic.com" crossorigin />
<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>
<link rel="stylesheet" crossorigin href="/assets/index-Bj_ETaak.css">
<script type="module" crossorigin src="/assets/index-CZzJtMVa.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-C4-IO3Fe.css">
</head>
<body>
<div id="root"></div>
+39 -8
View File
@@ -1,7 +1,8 @@
import { useState, useRef, useEffect } from 'react';
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 { useAuthStore } from '@/stores/authStore';
interface Meddelande {
id: string;
@@ -18,17 +19,19 @@ interface AgentChatProps {
export function AgentChat({ rum, isOpen, onToggle }: AgentChatProps) {
const context = getAgentContext(rum);
const { token } = useAuthStore();
const [meddelanden, setMeddelanden] = useState<Meddelande[]>([
{
id: 'welcome',
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()
}
]);
const [input, setInput] = useState('');
const [laddar, setLaddar] = useState(false);
const [isExpanded, setIsExpanded] = useState(false);
const [error, setError] = useState('');
const scrollRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLTextAreaElement>(null);
@@ -52,11 +55,16 @@ export function AgentChat({ rum, isOpen, onToggle }: AgentChatProps) {
setInput('');
setLaddar(true);
setError('');
try {
// Anropa Claude via OpenClaw API
const response = await fetch('/api/agent/chat', {
// Anropa BOC Agent API med auth
const response = await fetch('/api/v1/agents/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
headers: {
'Content-Type': 'application/json',
'Authorization': token ? `Bearer ${token}` : ''
},
body: JSON.stringify({
rum: context.rum,
systemPrompt: context.systemPrompt,
@@ -75,7 +83,19 @@ export function AgentChat({ rum, isOpen, onToggle }: AgentChatProps) {
innehall: data.svar,
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 {
const errData = await response.json().catch(() => ({}));
setError(errData.error || 'Agenten svarade inte');
// Fallback: Mock-svar
const mockSvar = genereraMockSvar(context, input.trim());
setMeddelanden(prev => [...prev, {
@@ -85,7 +105,8 @@ export function AgentChat({ rum, isOpen, onToggle }: AgentChatProps) {
tid: new Date().toISOString()
}]);
}
} catch (error) {
} catch (err) {
setError('Kunde inte nå agenten. Använder lokalt svar.');
const mockSvar = genereraMockSvar(context, input.trim());
setMeddelanden(prev => [...prev, {
id: `assistant-${Date.now()}`,
@@ -202,6 +223,16 @@ export function AgentChat({ rum, isOpen, onToggle }: AgentChatProps) {
)}
</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 */}
<div className="p-4 border-t border-border">
<div className="flex gap-2">
@@ -210,7 +241,7 @@ export function AgentChat({ rum, isOpen, onToggle }: AgentChatProps) {
value={input}
onChange={(e) => setInput(e.target.value)}
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"
rows={1}
/>
@@ -223,7 +254,7 @@ export function AgentChat({ rum, isOpen, onToggle }: AgentChatProps) {
</button>
</div>
<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>
</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,
UserPlus,
} from 'lucide-react'
import { AgentFAB } from '@/components/agent/AgentFAB'
const workflows = [
{
@@ -202,6 +203,9 @@ export function AutomationPage() {
))}
</div>
</Card>
{/* Automation Agent */}
<AgentFAB rum="automation" />
</div>
)
}
+4
View File
@@ -23,6 +23,7 @@ import {
Building2,
MoreHorizontal,
} from 'lucide-react'
import { AgentFAB } from '@/components/agent/AgentFAB'
interface Customer {
id: string
@@ -346,6 +347,9 @@ export function CRMPage() {
</Card>
)}
{/* CRM Agent */}
<AgentFAB rum="crm" />
{activeTab === 'Pipeline' && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-5">
{pipelineStages.length === 0 && (
+4
View File
@@ -31,6 +31,7 @@ import {
ArrowUpRight,
ArrowDownRight,
} from 'lucide-react'
import { AgentFAB } from '@/components/agent/AgentFAB'
interface BalanceAccount {
account: string
@@ -575,6 +576,9 @@ export function FinancePage() {
</div>
</Card>
)}
{/* Finance Agent */}
<AgentFAB rum="finance" />
</div>
)
}
+4
View File
@@ -22,6 +22,7 @@ import {
MoreHorizontal,
Clock,
} from 'lucide-react'
import { AgentFAB } from '@/components/agent/AgentFAB'
interface Employee {
id: string
@@ -347,6 +348,9 @@ export function HRPage() {
</Table>
</Card>
)}
{/* HR Agent */}
<AgentFAB rum="hr" />
</div>
)
}
+4
View File
@@ -19,6 +19,7 @@ import {
Briefcase,
Globe,
} from 'lucide-react'
import { AgentFAB } from '@/components/agent/AgentFAB'
interface Contract {
id: string
@@ -557,6 +558,9 @@ export function LegalPage() {
</div>
</div>
)}
{/* Legal Agent */}
<AgentFAB rum="legal" />
</div>
)
}
+4
View File
@@ -10,6 +10,7 @@ import {
TrendingUp,
Users,
} from 'lucide-react'
import { AgentFAB } from '@/components/agent/AgentFAB'
const campaigns = [
{ 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>
</Card>
{/* Marketing Agent */}
<AgentFAB rum="marketing" />
</div>
)
}
+4
View File
@@ -33,6 +33,7 @@ import {
Filter,
MoreHorizontal,
} from 'lucide-react'
import { AgentFAB } from '@/components/agent/AgentFAB'
interface Deal {
id: string
@@ -402,6 +403,9 @@ export function SalesPage() {
)}
</Card>
)}
{/* Sales Agent */}
<AgentFAB rum="sales" />
</div>
)
}
+4
View File
@@ -21,6 +21,7 @@ import {
Filter,
MoreHorizontal,
} from 'lucide-react'
import { AgentFAB } from '@/components/agent/AgentFAB'
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() },
@@ -212,6 +213,9 @@ export function SupportPage() {
</TableBody>
</Table>
</Card>
{/* Support Agent */}
<AgentFAB rum="support" />
</div>
)
}