ollie/kde/gui/PeerManagerWindow.qml

492 lines
18 KiB
QML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Window
Window {
id: peerWindow
title: "Peer Connections" + (currentSessionName ? " — " + currentSessionName : "")
width: 700
height: 500
minimumWidth: 500
minimumHeight: 400
property string currentSessionId: ollie.activeSessionId
property string currentAgentId: ollie.activeAgentId
property string currentSessionName: ""
property var agents: [] // [{id, name, x, y, peers: [name1, name2...]}]
property var selectedNode: null
property var connectingFrom: null // Node we're drawing connection from
function refresh() {
if (!currentSessionId) {
agents = []
return
}
var agentList = ollie.listAgentsInSession(currentSessionId)
var newAgents = []
var radius = Math.min(canvas.width, canvas.height) * 0.35
var centerX = canvas.width / 2
var centerY = canvas.height / 2
for (var i = 0; i < agentList.length; i++) {
var a = agentList[i]
// Skip sub-agents (depth > 0)
if (a.depth > 0) continue
var peers = ollie.listPeers(currentSessionId, a.id)
// Position in circle, or keep existing position
var existing = findAgent(a.id)
var angle = (2 * Math.PI * newAgents.length) / Math.max(agentList.length, 1) - Math.PI / 2
newAgents.push({
id: a.id,
name: a.name,
state: a.state,
x: existing ? existing.x : centerX + radius * Math.cos(angle),
y: existing ? existing.y : centerY + radius * Math.sin(angle),
peers: peers
})
}
agents = newAgents
canvas.requestPaint()
}
function findAgent(id) {
for (var i = 0; i < agents.length; i++) {
if (agents[i].id === id) return agents[i]
}
return null
}
function findAgentByName(name) {
for (var i = 0; i < agents.length; i++) {
if (agents[i].name === name) return agents[i]
}
return null
}
function nodeAt(x, y) {
var nodeRadius = 30
for (var i = agents.length - 1; i >= 0; i--) {
var a = agents[i]
var dx = x - a.x
var dy = y - a.y
if (dx * dx + dy * dy <= nodeRadius * nodeRadius) {
return a
}
}
return null
}
function updateSessionName() {
if (currentSessionId) {
var cfg = ollie.readSessionConfig(currentSessionId)
currentSessionName = cfg.name || currentSessionId
} else {
currentSessionName = ""
}
}
Timer {
id: refreshTimer
interval: 3000
running: peerWindow.visible
repeat: true
onTriggered: refresh()
}
onVisibleChanged: {
if (visible) {
currentSessionId = ollie.activeSessionId
currentAgentId = ollie.activeAgentId
updateSessionName()
refresh()
}
}
Connections {
target: ollie
function onActiveSessionIdChanged() {
if (peerWindow.visible) {
currentSessionId = ollie.activeSessionId
currentAgentId = ollie.activeAgentId
updateSessionName()
selectedNode = null
connectingFrom = null
refresh()
}
}
}
ColumnLayout {
anchors.fill: parent
anchors.margins: 8
spacing: 8
// Instructions
Label {
Layout.fillWidth: true
text: "Drag nodes to position. Click two nodes to connect/disconnect. Right-click for options."
opacity: 0.7
font.pixelSize: 11
}
// Canvas for graph
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
color: palette.base
border.color: palette.mid
border.width: 1
clip: true
Canvas {
id: canvas
anchors.fill: parent
property real panX: 0
property real panY: 0
property real zoom: 1.0
property point lastMouse
property bool panning: false
property var draggingNode: null
property point dragOffset
onPaint: {
var ctx = getContext("2d")
ctx.reset()
ctx.fillStyle = palette.base
ctx.fillRect(0, 0, width, height)
ctx.save()
ctx.translate(panX, panY)
ctx.scale(zoom, zoom)
var nodeRadius = 30
// Draw edges first
ctx.strokeStyle = palette.highlight
ctx.lineWidth = 2
for (var i = 0; i < agents.length; i++) {
var a = agents[i]
for (var j = 0; j < a.peers.length; j++) {
var peer = findAgentByName(a.peers[j])
if (peer && peer.id > a.id) { // Draw each edge once
ctx.beginPath()
ctx.moveTo(a.x, a.y)
ctx.lineTo(peer.x, peer.y)
ctx.stroke()
}
}
}
// Draw connection preview
if (connectingFrom && mouseArea.containsMouse) {
ctx.strokeStyle = "#888"
ctx.setLineDash([5, 5])
ctx.beginPath()
ctx.moveTo(connectingFrom.x, connectingFrom.y)
var mx = (mouseArea.mouseX - panX) / zoom
var my = (mouseArea.mouseY - panY) / zoom
ctx.lineTo(mx, my)
ctx.stroke()
ctx.setLineDash([])
}
// Draw nodes
for (var i = 0; i < agents.length; i++) {
var a = agents[i]
var isSelected = selectedNode && selectedNode.id === a.id
var isConnecting = connectingFrom && connectingFrom.id === a.id
var isActive = a.id === currentAgentId
// Node circle
ctx.beginPath()
ctx.arc(a.x, a.y, nodeRadius, 0, 2 * Math.PI)
// Fill based on state
if (a.state === "idle") {
ctx.fillStyle = "#4CAF50"
} else if (a.state === "thinking" || a.state === "calling") {
ctx.fillStyle = "#2196F3"
} else if (a.state === "paused") {
ctx.fillStyle = "#9E9E9E"
} else {
ctx.fillStyle = "#FF9800"
}
ctx.fill()
// Border
if (isSelected || isConnecting) {
ctx.strokeStyle = palette.highlightedText
ctx.lineWidth = 3
} else if (isActive) {
ctx.strokeStyle = palette.highlight
ctx.lineWidth = 2
} else {
ctx.strokeStyle = palette.text
ctx.lineWidth = 1
}
ctx.stroke()
// Label
ctx.fillStyle = palette.text
ctx.font = "12px sans-serif"
ctx.textAlign = "center"
ctx.textBaseline = "middle"
var label = a.name.length > 10 ? a.name.substring(0, 9) + "…" : a.name
ctx.fillText(label, a.x, a.y + nodeRadius + 15)
}
ctx.restore()
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onPressed: function(mouse) {
var x = (mouse.x - canvas.panX) / canvas.zoom
var y = (mouse.y - canvas.panY) / canvas.zoom
var node = nodeAt(x, y)
if (mouse.button === Qt.MiddleButton) {
canvas.panning = true
canvas.lastMouse = Qt.point(mouse.x, mouse.y)
} else if (mouse.button === Qt.RightButton && node) {
selectedNode = node
nodeContextMenu.popup()
} else if (mouse.button === Qt.LeftButton) {
if (node) {
if (connectingFrom) {
// Complete connection
if (connectingFrom.id !== node.id) {
toggleConnection(connectingFrom, node)
}
connectingFrom = null
} else {
// Start drag or connection
canvas.draggingNode = node
canvas.dragOffset = Qt.point(x - node.x, y - node.y)
selectedNode = node
}
} else {
// Click on empty space
if (connectingFrom) {
connectingFrom = null
} else {
selectedNode = null
}
}
canvas.requestPaint()
}
}
onReleased: function(mouse) {
if (canvas.panning) {
canvas.panning = false
}
if (canvas.draggingNode) {
canvas.draggingNode = null
}
}
onPositionChanged: function(mouse) {
if (canvas.panning) {
canvas.panX += mouse.x - canvas.lastMouse.x
canvas.panY += mouse.y - canvas.lastMouse.y
canvas.lastMouse = Qt.point(mouse.x, mouse.y)
canvas.requestPaint()
} else if (canvas.draggingNode) {
var x = (mouse.x - canvas.panX) / canvas.zoom
var y = (mouse.y - canvas.panY) / canvas.zoom
canvas.draggingNode.x = x - canvas.dragOffset.x
canvas.draggingNode.y = y - canvas.dragOffset.y
canvas.requestPaint()
} else if (connectingFrom) {
canvas.requestPaint()
}
}
onWheel: function(wheel) {
var factor = wheel.angleDelta.y > 0 ? 1.1 : 0.9
var newZoom = Math.max(0.3, Math.min(3.0, canvas.zoom * factor))
// Zoom toward mouse position
var mx = wheel.x
var my = wheel.y
canvas.panX = mx - (mx - canvas.panX) * (newZoom / canvas.zoom)
canvas.panY = my - (my - canvas.panY) * (newZoom / canvas.zoom)
canvas.zoom = newZoom
canvas.requestPaint()
}
onDoubleClicked: function(mouse) {
var x = (mouse.x - canvas.panX) / canvas.zoom
var y = (mouse.y - canvas.panY) / canvas.zoom
var node = nodeAt(x, y)
if (node) {
// Start connecting from this node
connectingFrom = node
canvas.requestPaint()
}
}
}
Menu {
id: nodeContextMenu
MenuItem {
text: "Connect to..."
enabled: selectedNode !== null
onTriggered: {
connectingFrom = selectedNode
canvas.requestPaint()
}
}
MenuItem {
text: "Disconnect all"
enabled: selectedNode !== null && selectedNode.peers.length > 0
onTriggered: {
if (selectedNode) {
for (var i = 0; i < selectedNode.peers.length; i++) {
ollie.removePeer(currentSessionId, selectedNode.id, selectedNode.peers[i])
}
refresh()
}
}
}
MenuSeparator {}
MenuItem {
text: "Switch to agent"
enabled: selectedNode !== null
onTriggered: {
if (selectedNode) {
ollie.switchAgent(currentSessionId, selectedNode.id)
}
}
}
}
}
// Zoom controls
RowLayout {
anchors.bottom: parent.bottom
anchors.right: parent.right
anchors.margins: 8
spacing: 4
Button {
text: "−"
implicitWidth: 30
implicitHeight: 30
onClicked: {
canvas.zoom = Math.max(0.3, canvas.zoom * 0.8)
canvas.requestPaint()
}
}
Label {
text: Math.round(canvas.zoom * 100) + "%"
Layout.preferredWidth: 45
horizontalAlignment: Text.AlignHCenter
}
Button {
text: "+"
implicitWidth: 30
implicitHeight: 30
onClicked: {
canvas.zoom = Math.min(3.0, canvas.zoom * 1.25)
canvas.requestPaint()
}
}
Button {
text: "Reset"
implicitWidth: 50
implicitHeight: 30
onClicked: {
canvas.zoom = 1.0
canvas.panX = 0
canvas.panY = 0
refresh() // Re-layout nodes
}
}
}
}
// Action buttons
RowLayout {
Layout.fillWidth: true
spacing: 8
Button {
text: "Refresh"
icon.name: "view-refresh"
onClicked: refresh()
}
Button {
text: "Auto Layout"
icon.name: "view-grid"
onClicked: {
// Re-position all nodes in a circle
var radius = Math.min(canvas.width, canvas.height) * 0.35 / canvas.zoom
var centerX = (canvas.width / 2 - canvas.panX) / canvas.zoom
var centerY = (canvas.height / 2 - canvas.panY) / canvas.zoom
for (var i = 0; i < agents.length; i++) {
var angle = (2 * Math.PI * i) / agents.length - Math.PI / 2
agents[i].x = centerX + radius * Math.cos(angle)
agents[i].y = centerY + radius * Math.sin(angle)
}
canvas.requestPaint()
}
}
Item { Layout.fillWidth: true }
Label {
visible: connectingFrom !== null
text: "Click another node to connect, or empty space to cancel"
color: palette.highlight
}
Item { Layout.fillWidth: true }
Button {
text: "Close"
icon.name: "window-close"
onClicked: peerWindow.close()
}
}
}
function toggleConnection(a, b) {
// Check if already connected
var connected = false
for (var i = 0; i < a.peers.length; i++) {
if (a.peers[i] === b.name) {
connected = true
break
}
}
if (connected) {
ollie.removePeer(currentSessionId, a.id, b.name)
} else {
ollie.addPeer(currentSessionId, a.id, b.name)
}
refresh()
}
}