feat(agent): integrate agents into all BOC modules
- 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:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user