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
+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>
)
}