| <script lang="ts"> |
| import { marked } from 'marked'; |
|
|
| export let gradio: any; |
| export let elem_id: string = ""; |
| export let elem_classes: string[] = []; |
| export let visible: boolean = true; |
| export let value: string = "{}"; |
| export let label: string = "Consilium Roundtable"; |
| export let label_icon: string | null = "🎭"; |
| export let show_label: boolean = true; |
| export let scale: number | null = null; |
| export let min_width: number | undefined = undefined; |
|
|
| $: containerClasses = `wrapper ${elem_classes.join(' ')}`; |
| $: containerStyle = scale ? `--scale: ${scale}` : ''; |
| $: minWidthStyle = min_width ? `min-width: ${min_width}px` : ''; |
|
|
| let participants = []; |
| let messages = []; |
| let currentSpeaker = null; |
| let thinking = []; |
| let showBubbles = []; |
| let avatarImages = {}; |
| |
| function updateFromValue() { |
| try { |
| const parsedValue = JSON.parse(value); |
| |
| participants = parsedValue.participants || []; |
| messages = parsedValue.messages || []; |
| currentSpeaker = parsedValue.currentSpeaker || null; |
| thinking = parsedValue.thinking || []; |
| showBubbles = parsedValue.showBubbles || []; |
| avatarImages = parsedValue.avatarImages || {}; |
| |
| console.log("Clean JSON parsed:", {participants, messages, currentSpeaker, thinking, showBubbles, avatarImages}); |
| } catch (e) { |
| console.error("Invalid JSON:", value, e); |
| } |
| } |
|
|
| function renderMarkdown(text: string): string { |
| if (!text) return text; |
| |
| try { |
| |
| marked.setOptions({ |
| breaks: true, |
| gfm: true, |
| sanitize: false, |
| smartypants: false |
| }); |
| |
| |
| const hasMultipleLines = text.includes('\n'); |
| |
| if (hasMultipleLines) { |
| return marked.parse(text); |
| } else { |
| return marked.parseInline(text); |
| } |
| } catch (error) { |
| console.error('Markdown parsing error:', error); |
| return text; |
| } |
| } |
| |
| $: value, updateFromValue(); |
| |
| const avatarEmojis = { |
| "Anthropic": "🤖", |
| "Claude": "🤖", |
| "Search": "🔍", |
| "Web Search Agent": "🔍", |
| "OpenAI": "🧠", |
| "GPT-4": "🧠", |
| "Google": "💎", |
| "Gemini": "💎", |
| "QwQ-32B": "😊", |
| "DeepSeek-R1": "🔮", |
| "Mistral": "🐱", |
| "Mistral Large": "🐱", |
| "Meta-Llama-3.1-8B": "🦙" |
| }; |
| |
| function getEmoji(name: string) { |
| return avatarEmojis[name] || "🤖"; |
| } |
|
|
| function getAvatarImageUrl(name: string) { |
| return avatarImages[name] || null; |
| } |
|
|
| function hasCustomImage(name: string) { |
| return avatarImages[name] && avatarImages[name].trim() !== ''; |
| } |
| |
| function getLatestMessage(speaker: string) { |
| if (thinking.includes(speaker)) { |
| return `${speaker} is thinking...`; |
| } |
| if (currentSpeaker === speaker) { |
| return `${speaker} is responding...`; |
| } |
| |
| const speakerMessages = messages.filter(m => m.speaker === speaker); |
| if (speakerMessages.length === 0) { |
| return `${speaker} is ready to discuss...`; |
| } |
| return speakerMessages[speakerMessages.length - 1].text || `${speaker} responded`; |
| } |
| |
| function isBubbleVisible(speaker: string) { |
| const isThinking = thinking.includes(speaker); |
| const isSpeaking = currentSpeaker === speaker; |
| const shouldShow = showBubbles.includes(speaker); |
| const visible = isThinking || isSpeaking || shouldShow; |
| |
| console.log(`${speaker} bubble visible:`, visible, {isThinking, isSpeaking, shouldShow}); |
| return visible; |
| } |
| |
| function isAvatarActive(speaker: string) { |
| return thinking.includes(speaker) || currentSpeaker === speaker; |
| } |
| |
| function getPosition(index: number, total: number) { |
| const angle = (360 / total) * index; |
| const radians = (angle - 90) * (Math.PI / 180); |
| |
| const radiusX = 260; |
| const radiusY = 180; |
| |
| const x = Math.cos(radians) * radiusX; |
| const y = Math.sin(radians) * radiusY; |
| |
| return { |
| left: `calc(50% + ${x}px)`, |
| top: `calc(50% + ${y}px)`, |
| transform: 'translate(-50%, -50%)' |
| }; |
| } |
|
|
| function handleImageError(event: Event, participant: string) { |
| console.warn(`Failed to load avatar image for ${participant}, falling back to emoji`); |
| |
| avatarImages = {...avatarImages, [participant]: null}; |
| } |
|
|
| function handleLabelIconError(event: Event) { |
| console.warn('Failed to load label icon image, falling back to default emoji'); |
| |
| label_icon = null; |
| } |
|
|
| function isImageUrl(str: string | null): boolean { |
| if (!str) return false; |
| return str.startsWith('http://') || str.startsWith('https://') || str.startsWith('data:'); |
| } |
| </script> |
|
|
| <div |
| class={containerClasses} |
| class:hidden={!visible} |
| id={elem_id} |
| style="{containerStyle}; {minWidthStyle}" |
| > |
| <div class="consilium-container" id="consilium-roundtable"> |
| <div class="table-center"> |
| {#if show_label && label} |
| <label class="block-title" for="consilium-roundtable"> |
| {#if label_icon} |
| <div class="label-icon-container"> |
| {#if isImageUrl(label_icon)} |
| <img |
| src={label_icon} |
| alt="Label Icon" |
| class="label-icon-image" |
| on:error={handleLabelIconError} |
| /> |
| {:else} |
| <span class="label-icon-emoji">{label_icon}</span> |
| {/if} |
| </div> |
| {/if} |
| {label} |
| </label> |
| {/if} |
| </div> |
| |
| <div class="participants-circle"> |
| {#each participants as participant, index} |
| <div |
| class="participant-seat" |
| style="left: {getPosition(index, participants.length).left}; top: {getPosition(index, participants.length).top}; transform: {getPosition(index, participants.length).transform};" |
| > |
| <div class="speech-bubble" class:visible={isBubbleVisible(participant)}> |
| <div class="bubble-content">{@html renderMarkdown(getLatestMessage(participant))}</div> |
| <div class="bubble-arrow"></div> |
| </div> |
| |
| <div |
| class="avatar" |
| class:speaking={isAvatarActive(participant)} |
| class:thinking={thinking.includes(participant)} |
| class:responding={currentSpeaker === participant} |
| class:has-image={hasCustomImage(participant)} |
| role="button" |
| tabindex="0" |
| > |
| {#if hasCustomImage(participant)} |
| <img |
| src={getAvatarImageUrl(participant)} |
| alt={participant} |
| class="avatar-image" |
| on:error={(event) => handleImageError(event, participant)} |
| /> |
| {:else} |
| <span class="avatar-emoji">{getEmoji(participant)}</span> |
| {/if} |
| </div> |
| <div class="participant-name">{participant}</div> |
| </div> |
| {/each} |
| </div> |
| </div> |
| </div> |
|
|
| <style> |
| .hidden { |
| display: none; |
| } |
| |
| .block-title { |
| padding: 10px; |
| font-weight: bold; |
| color: #ffd700; |
| display: flex; |
| flex-wrap: wrap; |
| justify-content: center; |
| text-shadow: 0 2px 4px rgba(0,0,0,0.8); |
| } |
| |
| .label-icon-container { |
| width: 24px; |
| height: 24px; |
| } |
| |
| .label-icon-emoji { |
| font-size: 1.2rem; |
| line-height: 1; |
| } |
| |
| .label-icon-image { |
| width: 24px; |
| height: 24px; |
| object-fit: contain; |
| border-radius: 4px; |
| } |
| |
| .wrapper { |
| width: 600px; |
| height: 600px; |
| position: relative; |
| } |
| |
| .consilium-container { |
| top: 190px; |
| position: relative; |
| width: 450px; |
| height: 300px; |
| margin: 20px auto; |
| border-radius: 50%; |
| background: linear-gradient(135deg, #0f5132, #198754); |
| border: 8px solid #8b4513; |
| box-shadow: |
| 0 8px 32px rgba(0,0,0,0.4), |
| inset 0 0 20px rgba(0,0,0,0.2); |
| } |
| |
| .table-center { |
| position: absolute; |
| top: 50%; |
| left: 50%; |
| transform: translate(-50%, -50%); |
| text-align: center; |
| background: rgba(0,0,0,0.3); |
| border-radius: 50%; |
| width: 140px; |
| height: 100px; |
| display: flex; |
| flex-direction: column; |
| align-items: center; |
| justify-content: center; |
| border: 2px solid #8b4513; |
| box-shadow: inset 0 0 10px rgba(0,0,0,0.5); |
| } |
| |
| .participant-seat { |
| position: absolute; |
| display: flex; |
| flex-direction: column; |
| align-items: center; |
| } |
| |
| .avatar { |
| width: 60px; |
| height: 60px; |
| border-radius: 50%; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| font-size: 1.4rem; |
| background: linear-gradient(145deg, #ffffff, #e6e6e6); |
| border: 3px solid #8b4513; |
| box-shadow: |
| 0 6px 15px rgba(0,0,0,0.3), |
| inset 0 2px 5px rgba(255,255,255,0.5); |
| margin-bottom: 8px; |
| transition: all 0.3s ease; |
| position: relative; |
| z-index: 10; |
| overflow: hidden; |
| } |
| |
| .avatar.has-image { |
| background: #f8f9fa; |
| padding: 2px; |
| } |
| |
| .avatar-image { |
| width: 100%; |
| height: 100%; |
| object-fit: cover; |
| border-radius: 50%; |
| } |
| |
| .avatar-emoji { |
| font-size: 1.4rem; |
| line-height: 1; |
| } |
| |
| .avatar.thinking { |
| border-color: #ff6b35; |
| animation: thinking-pulse 1.5s infinite; |
| } |
| |
| .avatar.responding { |
| border-color: #ffd700; |
| animation: speaking-glow 1s infinite; |
| } |
| |
| .avatar.speaking { |
| border-color: #ffd700; |
| } |
| |
| .participant-name { |
| font-size: 0.75rem; |
| font-weight: bold; |
| color: #ffd700; |
| text-shadow: 0 2px 4px rgba(0,0,0,0.8); |
| text-align: center; |
| white-space: nowrap; |
| background: rgba(0,0,0,0.3); |
| padding: 2px 8px; |
| border-radius: 10px; |
| border: 1px solid #8b4513; |
| } |
| |
| .speech-bubble { |
| position: absolute; |
| bottom: 90px; |
| left: 50%; |
| transform: translateX(-50%) translateY(20px); |
| background: white; |
| border-radius: 15px; |
| padding: 10px 14px; |
| box-shadow: 0 8px 25px rgba(0,0,0,0.3); |
| z-index: 20; |
| opacity: 0; |
| transition: all 0.4s ease; |
| pointer-events: none; |
| border: 2px solid #8b4513; |
| min-width: 180px; |
| max-width: 300px; |
| word-wrap: break-word; |
| white-space: normal; |
| } |
| |
| .speech-bubble.visible { |
| opacity: 1; |
| transform: translateX(-50%) translateY(0); |
| pointer-events: auto; |
| } |
| |
| .bubble-content { |
| font-size: 0.8rem; |
| color: #333; |
| line-height: 1.4; |
| text-align: left; |
| max-height: 100px; |
| overflow-y: auto; |
| scrollbar-width: thin; |
| scrollbar-color: #8b4513 #f0f0f0; |
| } |
| |
| .bubble-content::-webkit-scrollbar { |
| width: 6px; |
| } |
| |
| .bubble-content::-webkit-scrollbar-track { |
| background: #f0f0f0; |
| border-radius: 3px; |
| } |
| |
| .bubble-content::-webkit-scrollbar-thumb { |
| background: #8b4513; |
| border-radius: 3px; |
| } |
| |
| .bubble-content::-webkit-scrollbar-thumb:hover { |
| background: #654321; |
| } |
| |
| .bubble-arrow { |
| position: absolute; |
| bottom: -10px; |
| left: 50%; |
| transform: translateX(-50%); |
| width: 0; |
| height: 0; |
| border-left: 10px solid transparent; |
| border-right: 10px solid transparent; |
| border-top: 10px solid white; |
| } |
| |
| .bubble-arrow::before { |
| content: ''; |
| position: absolute; |
| bottom: 2px; |
| left: 50%; |
| transform: translateX(-50%); |
| width: 0; |
| height: 0; |
| border-left: 12px solid transparent; |
| border-right: 12px solid transparent; |
| border-top: 12px solid #8b4513; |
| } |
| |
| @keyframes thinking-pulse { |
| 0%, 100% { |
| transform: scale(1); |
| box-shadow: 0 6px 15px rgba(0,0,0,0.3), 0 0 15px rgba(255, 107, 53, 0.4); |
| } |
| 50% { |
| transform: scale(1.03); |
| box-shadow: 0 8px 20px rgba(0,0,0,0.4), 0 0 25px rgba(255, 107, 53, 0.6); |
| } |
| } |
| |
| @keyframes speaking-glow { |
| 0%, 100% { |
| box-shadow: 0 6px 15px rgba(0,0,0,0.3), 0 0 20px rgba(255, 215, 0, 0.5); |
| } |
| 50% { |
| box-shadow: 0 8px 20px rgba(0,0,0,0.4), 0 0 30px rgba(255, 215, 0, 0.8); |
| } |
| } |
| </style> |