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>
</>
);
}