Skip to content

Commit 50f8c6a

Browse files
committed
icons and look and feel
1 parent 6c1eb4f commit 50f8c6a

28 files changed

Lines changed: 764 additions & 273 deletions

pom.xml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
<groupId>uno.anahata</groupId>
88
<artifactId>gemini-java-client</artifactId>
9-
<version>1.0.13</version>
9+
<version>1.0.14-SNAPSHOT</version>
1010
<packaging>jar</packaging>
1111

1212
<name>gemini-java-client</name>
@@ -412,6 +412,11 @@
412412
<artifactId>slf4j-jdk14</artifactId>
413413
<version>2.0.17</version>
414414
</dependency>
415+
<dependency>
416+
<groupId>com.formdev</groupId>
417+
<artifactId>flatlaf-intellij-themes</artifactId>
418+
<version>3.6.2</version>
419+
</dependency>
415420
</dependencies>
416421
</profile>
417422

src/main/java/uno/anahata/ai/swing/ChatPanel.java

Lines changed: 33 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
import uno.anahata.ai.status.StatusListener;
2828
import uno.anahata.ai.context.provider.ContextProvider;
2929
import uno.anahata.ai.gemini.GeminiAPI;
30+
import uno.anahata.ai.swing.SwingChatConfig.ThemeMode;
31+
import uno.anahata.ai.swing.SwingChatConfig.UITheme;
3032

3133
@Slf4j
3234
@Getter
@@ -43,6 +45,7 @@ public class ChatPanel extends JPanel implements ContextListener, StatusListener
4345
private JButton loadSessionButton;
4446
private JToggleButton localToolsButton;
4547
private JToggleButton serverToolsButton;
48+
private JComboBox<ThemeMode> themeModeComboBox;
4649
private JComboBox<Model> modelIdComboBox;
4750

4851
private final EditorKitProvider editorKitProvider;
@@ -83,6 +86,9 @@ public ChatPanel(SwingChatConfig config, EditorKitProvider editorKitProvider) {
8386
this.config = config;
8487
this.editorKitProvider = editorKitProvider;
8588

89+
// CRITICAL: Initialize the UITheme with the correct config BEFORE creating any UI components
90+
UITheme.refresh(config);
91+
8692
FunctionPrompter prompter = new SwingFunctionPrompter(this);
8793
this.chat = new Chat(config, prompter);
8894
this.chat.addContextListener(this);
@@ -112,7 +118,7 @@ private void initComponents() {
112118
}
113119
});
114120

115-
serverToolsButton = new JToggleButton(IconUtils.getIcon("google.png", 18), chat.isServerToolsEnabled());
121+
serverToolsButton = new JToggleButton(IconUtils.getIcon("google.png"), chat.isServerToolsEnabled());
116122
serverToolsButton.setToolTipText("Enable / Disable Server Tools (Google Search)");
117123
serverToolsButton.addActionListener(e -> {
118124
boolean selected = serverToolsButton.isSelected();
@@ -159,8 +165,27 @@ private void initComponents() {
159165

160166
add(toolbar, BorderLayout.WEST);
161167

162-
// --- NORTH Panel (Model ID only) ---
168+
// --- NORTH Panel (Model ID and Theme) ---
163169
JPanel northPanel = new JPanel(new BorderLayout());
170+
171+
// Theme Selector (Moved to the LEFT)
172+
themeModeComboBox = new JComboBox<>(ThemeMode.values());
173+
themeModeComboBox.setSelectedItem(config.getThemeMode());
174+
themeModeComboBox.addActionListener(e -> {
175+
ThemeMode selected = (ThemeMode) themeModeComboBox.getSelectedItem();
176+
if (selected != null) {
177+
config.setThemeMode(selected);
178+
UITheme.refresh(config);
179+
chatPanel.redraw();
180+
heatmapPanel.updateContext(chat.getContext()); // Refresh heatmap colors
181+
}
182+
});
183+
184+
JPanel themePanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
185+
themePanel.add(new JLabel("Theme:"));
186+
themePanel.add(themeModeComboBox);
187+
northPanel.add(themePanel, BorderLayout.WEST);
188+
164189
modelIdComboBox = new JComboBox<>();
165190
modelIdComboBox.setRenderer(new DefaultListCellRenderer() {
166191
@Override
@@ -189,10 +214,11 @@ public Component getListCellRendererComponent(JList<?> list, Object value, int i
189214
}
190215
});
191216

192-
JPanel modelIdPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
193-
modelIdPanel.add(new JLabel("Model:"));
194-
modelIdPanel.add(modelIdComboBox);
195-
northPanel.add(modelIdPanel, BorderLayout.EAST);
217+
JPanel modelPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
218+
modelPanel.add(new JLabel("Model:"));
219+
modelPanel.add(modelIdComboBox);
220+
northPanel.add(modelPanel, BorderLayout.EAST);
221+
196222
add(northPanel, BorderLayout.NORTH);
197223

198224
// --- SOUTH Panel (Input and Status) ---
@@ -202,7 +228,6 @@ public Component getListCellRendererComponent(JList<?> list, Object value, int i
202228
JPanel mainSouthPanel = new JPanel(new BorderLayout());
203229
mainSouthPanel.add(inputPanel, BorderLayout.CENTER);
204230
mainSouthPanel.add(statusPanel, BorderLayout.SOUTH);
205-
// We will add this to the split pane later, not directly to the main panel.
206231

207232
// --- CENTER Panel (Tabs) ---
208233
chatPanel = new ConversationPanel(this);
@@ -239,7 +264,6 @@ public Component getListCellRendererComponent(JList<?> list, Object value, int i
239264
mainSplitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, tabbedPane, mainSouthPanel);
240265
mainSplitPane.setResizeWeight(0.8); // Give more space to the chat history initially
241266
mainSplitPane.setOneTouchExpandable(true);
242-
// By removing the setBorder call, we restore the default L&F border, making the divider visible.
243267

244268
add(mainSplitPane, BorderLayout.CENTER);
245269

@@ -334,24 +358,6 @@ public void checkAutobackupOrStartupContent() {
334358
boolean restoreAttempted = false;
335359

336360
if (autobackupFile.exists() && autobackupFile.length() > 0) {
337-
338-
/*
339-
int response = JOptionPane.showConfirmDialog(
340-
this,
341-
"An automatic backup from a previous session was found. \n"
342-
+ "\n\n" + autobackupFile + ""
343-
+ "\n\nDo you want to restore it?",
344-
345-
"Anahata AI - Restore Session? " + autobackupFile.getName(),
346-
JOptionPane.YES_NO_OPTION,
347-
JOptionPane.QUESTION_MESSAGE
348-
);
349-
if (response == JOptionPane.YES_OPTION) {
350-
restoreAttempted = true;
351-
loadAutobackupInSwingWorker();
352-
}*/
353-
354-
//trying always load
355361
restoreAttempted = true;
356362
loadAutobackupInSwingWorker();
357363
}
@@ -458,4 +464,4 @@ private void loadSession() {
458464
JOptionPane.showMessageDialog(this, "Error loading session: " + ex.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
459465
}
460466
}
461-
}
467+
}

src/main/java/uno/anahata/ai/swing/ContextHeatmapPanel.java

Lines changed: 36 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@
5757
import uno.anahata.ai.tools.ToolManager;
5858
import uno.anahata.ai.internal.GsonUtils;
5959
import uno.anahata.ai.internal.PartUtils;
60+
import uno.anahata.ai.swing.SwingChatConfig.UITheme;
61+
import uno.anahata.ai.swing.render.PartType;
6062

6163
@Slf4j
6264
public class ContextHeatmapPanel extends JPanel {
@@ -68,7 +70,6 @@ public class ContextHeatmapPanel extends JPanel {
6870
private JLabel statusLabel;
6971
private SwingWorker<List<PartInfo>, Void> worker;
7072
private ToolManager toolManager;
71-
private SwingChatConfig.UITheme theme;
7273

7374
private ScrollableTooltipPopup tooltipPopup;
7475

@@ -78,7 +79,6 @@ public ContextHeatmapPanel() {
7879

7980
public void setToolManager(ToolManager toolManager) {
8081
this.toolManager = toolManager;
81-
this.theme = new SwingChatConfig.UITheme();
8282
}
8383

8484
private void initComponents() {
@@ -249,7 +249,7 @@ private List<PartInfo> buildPartInfoList(List<ChatMessage> context) {
249249
if (msg.getContent() != null && msg.getContent().parts().isPresent()) {
250250
List<Part> parts = msg.getContent().parts().get();
251251
for (int j = 0; j < parts.size(); j++) {
252-
infos.add(new PartInfo(i, j, msg, parts.get(j), toolManager, theme, statusMap));
252+
infos.add(new PartInfo(i, j, msg, parts.get(j), toolManager, statusMap));
253253
}
254254
}
255255
}
@@ -262,7 +262,7 @@ public static class PartInfo {
262262
private final int partIndex;
263263
private final long messageSeqId;
264264
private final String role;
265-
private final String partType;
265+
private final PartType partType;
266266
private final long sizeInBytes;
267267
private final String functionName;
268268
private final String resourceId;
@@ -271,35 +271,30 @@ public static class PartInfo {
271271
private final String contentSummary;
272272
private final String fullContentText;
273273
private final boolean isError;
274-
private final Color roleColor;
275274
private final Part part;
276275

277-
PartInfo(int msgIdx, int partIdx, ChatMessage msg, Part part, ToolManager fm, SwingChatConfig.UITheme theme, Map<String, ResourceStatus> statusMap) {
276+
PartInfo(int msgIdx, int partIdx, ChatMessage msg, Part part, ToolManager fm, Map<String, ResourceStatus> statusMap) {
278277
this.messageIndex = msgIdx;
279278
this.partIndex = partIdx;
280279
this.messageSeqId = msg.getSequentialId();
281280
this.role = msg.getContent().role().orElse("unknown");
282281
this.sizeInBytes = PartUtils.calculateSizeInBytes(part);
283-
this.roleColor = getRoleColor(theme, this.role);
284282
this.part = part;
283+
this.partType = PartType.from(part);
285284

286-
String tempPartType = "Unknown";
287285
String tempFullContent = part.toString();
288286
String tempFuncName = "";
289287
String tempResourceId = "";
290288
ResourceStatus tempResourceStatus = null;
291289
boolean tempIsError = false;
292290

293291
if (part.text().isPresent()) {
294-
tempPartType = "Text";
295292
tempFullContent = part.text().get();
296293
} else if (part.functionCall().isPresent()) {
297-
tempPartType = "FunctionCall";
298294
tempFuncName = part.functionCall().get().name().orElse("");
299295
tempFullContent = "Call: " + tempFuncName + "\nArgs: " + part.functionCall().get().args();
300296
} else if (part.functionResponse().isPresent()) {
301297
FunctionResponse fr = part.functionResponse().get();
302-
tempPartType = "FunctionResponse";
303298
tempFuncName = fr.name().orElse("");
304299
Map<String, Object> respMap = (Map<String, Object>) fr.response().get();
305300
tempFullContent = "Response: " + tempFuncName + "\nContent: " + respMap;
@@ -316,11 +311,14 @@ public static class PartInfo {
316311
}
317312
}
318313
} else if (part.inlineData().isPresent()) {
319-
tempPartType = "Blob";
320314
tempFullContent = "MIME Type: " + part.inlineData().get().mimeType().orElse("") + ", Size: " + sizeInBytes + " bytes";
315+
} else if (part.codeExecutionResult().isPresent()) {
316+
tempFullContent = part.codeExecutionResult().get().output().orElse("");
317+
tempIsError = "ERROR".equalsIgnoreCase(part.codeExecutionResult().get().outcome().map(Object::toString).orElse(""));
318+
} else if (part.executableCode().isPresent()) {
319+
tempFullContent = part.executableCode().get().code().orElse("");
321320
}
322321

323-
this.partType = tempPartType;
324322
this.fullContentText = tempFullContent;
325323
this.functionName = tempFuncName;
326324
this.resourceId = tempResourceId;
@@ -338,15 +336,6 @@ private String extractFilename(String path) {
338336
return path;
339337
}
340338
}
341-
342-
private Color getRoleColor(SwingChatConfig.UITheme theme, String role) {
343-
switch (role) {
344-
case "user": return theme.getUserHeaderBg();
345-
case "model": return theme.getModelHeaderBg();
346-
case "tool": return theme.getToolHeaderBg();
347-
default: return theme.getDefaultHeaderBg();
348-
}
349-
}
350339
}
351340

352341
private static class PartTableModel extends AbstractTableModel {
@@ -381,7 +370,7 @@ public Object getValueAt(int rowIndex, int columnIndex) {
381370
case 0: return info.getMessageIndex();
382371
case 1: return info.getPartIndex();
383372
case 2: return info.getRole();
384-
case 3: return info.getPartType();
373+
case 3: return info.getPartType().name();
385374
case 4: return info.getSizeInBytes();
386375
case 5: return info.getFunctionName();
387376
case 6: return info.getResourceIdFilename();
@@ -401,12 +390,13 @@ public Component getTableCellRendererComponent(JTable table, Object value, boole
401390
if (!isSelected) {
402391
int modelRow = table.convertRowIndexToModel(row);
403392
PartInfo info = tableModel.getPartInfo(modelRow);
393+
UITheme theme = UITheme.get();
404394
if (info.isError()) {
405-
c.setBackground(theme.getFunctionErrorBg());
406-
c.setForeground(theme.getFunctionErrorFg());
395+
c.setBackground(theme.getErrorBg());
396+
c.setForeground(theme.getErrorFg());
407397
} else {
408-
c.setBackground(info.getRoleColor());
409-
c.setForeground(theme.getFontColor());
398+
c.setBackground(theme.getHeatmapRowBg(info.getRole()));
399+
c.setForeground(theme.getHeatmapRowFg(info.getRole()));
410400
}
411401
}
412402
return c;
@@ -418,15 +408,6 @@ private static class PieChartPanel extends JPanel {
418408
private final JTable table;
419409
private final PartTableModel tableModel;
420410
private static final DecimalFormat PERCENT_FORMAT = new DecimalFormat("#.0%");
421-
private static final Map<String, Color> PART_TYPE_COLORS = new HashMap<>();
422-
423-
static {
424-
PART_TYPE_COLORS.put("Text", new Color(0, 120, 215));
425-
PART_TYPE_COLORS.put("FunctionCall", new Color(216, 59, 1));
426-
PART_TYPE_COLORS.put("FunctionResponse", new Color(0, 153, 188));
427-
PART_TYPE_COLORS.put("Blob", new Color(104, 33, 122));
428-
PART_TYPE_COLORS.put("Unknown", Color.GRAY);
429-
}
430411

431412
public PieChartPanel(JTable table, PartTableModel tableModel) {
432413
this.table = table;
@@ -497,9 +478,10 @@ private void processData(List<PartInfo> data) {
497478
}
498479
List<Slice> newSlices = new ArrayList<>();
499480
double currentAngle = 0;
481+
UITheme theme = UITheme.get();
500482
for (PartInfo info : data) {
501483
double angle = (double) info.getSizeInBytes() / totalSize * 360.0;
502-
Color color = PART_TYPE_COLORS.getOrDefault(info.getPartType(), Color.DARK_GRAY);
484+
Color color = theme.getPieColor(info.getRole(), info.isError());
503485
newSlices.add(new Slice(currentAngle, angle, color, info, (double) info.getSizeInBytes() / totalSize));
504486
currentAngle += angle;
505487
}
@@ -569,7 +551,8 @@ private void drawLabels(Graphics2D g2d, List<LabelInfo> labels, FontMetrics fm)
569551
adjustLabelPositions(leftLabels, fm);
570552

571553
// Draw all labels and lines
572-
g2d.setColor(Color.BLACK);
554+
UITheme theme = UITheme.get();
555+
g2d.setColor(theme.getFontColor());
573556
for (LabelInfo label : labels) {
574557
g2d.drawLine((int) label.edgePoint.getX(), (int) label.edgePoint.getY(), (int) label.leaderLineEnd.getX(), (int) label.leaderLineEnd.getY());
575558
g2d.drawLine((int) label.leaderLineEnd.getX(), (int) label.leaderLineEnd.getY(), (int) label.horizontalLineEnd.getX(), (int) label.horizontalLineEnd.getY());
@@ -598,7 +581,7 @@ public String getToolTipText(MouseEvent e) {
598581
if (slice != null) {
599582
PartInfo info = slice.info;
600583
return String.format("<html><b>%s</b> (Msg %d, Part %d)<br>Size: %d bytes (%.2f%%)<br>Type: %s<br>Summary: %s</html>",
601-
info.getRole(), info.getMessageIndex(), info.getPartIndex(), info.getSizeInBytes(), slice.percentage * 100, info.getPartType(), info.getContentSummary());
584+
info.getRole(), info.getMessageIndex(), info.getPartIndex(), info.getSizeInBytes(), slice.percentage * 100, info.getPartType().name(), info.getContentSummary());
602585
}
603586
return null;
604587
}
@@ -622,10 +605,19 @@ private static class LabelInfo {
622605

623606
LabelInfo(Slice slice, double midAngleRad, int diameter, double sliceCenterX, double sliceCenterY) {
624607
this.midAngleRad = midAngleRad;
625-
this.text = String.format("%s (%s)",
626-
slice.info.getResourceIdFilename().isEmpty() ? slice.info.getPartType() : slice.info.getResourceIdFilename(),
627-
PERCENT_FORMAT.format(slice.percentage)
628-
);
608+
609+
String gist;
610+
if (!slice.info.getResourceIdFilename().isEmpty()) {
611+
gist = slice.info.getResourceIdFilename();
612+
} else if (slice.info.getPartType() == PartType.TEXT) {
613+
gist = StringUtils.capitalize(slice.info.getRole());
614+
} else if (slice.info.getPartType() == PartType.FUNCTION_CALL || slice.info.getPartType() == PartType.FUNCTION_RESPONSE) {
615+
gist = slice.info.getFunctionName();
616+
} else {
617+
gist = slice.info.getPartType().name();
618+
}
619+
620+
this.text = String.format("%s (%s)", gist, PERCENT_FORMAT.format(slice.percentage));
629621

630622
double radius = diameter / 2.0;
631623
double labelRadius = radius + 15;
@@ -665,4 +657,4 @@ void adjustY(double amount) {
665657
}
666658
}
667659
}
668-
}
660+
}

src/main/java/uno/anahata/ai/swing/ConversationPanel.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,15 @@ public void componentResized(ComponentEvent e) {
7272

7373
chatScrollPane = new JScrollPane(chatContentPanel);
7474
chatScrollPane.setBorder(BorderFactory.createEmptyBorder());
75+
76+
// Track the "at bottom" state in real-time as the user scrolls.
77+
// This prevents jumping to the bottom when interacting with elements (like toggling tool output)
78+
// if the user has scrolled up to read previous messages.
79+
chatScrollPane.getVerticalScrollBar().addAdjustmentListener(e -> {
80+
if (!e.getValueIsAdjusting()) {
81+
wasAtBottom = isScrolledToBottom();
82+
}
83+
});
7584

7685
JButton scrollToBottomButton = new JButton("↓");
7786
scrollToBottomButton.setToolTipText("Scroll to Bottom");
@@ -91,9 +100,9 @@ public void redraw() {
91100
chatContentPanel.removeAll();
92101
List<ChatMessage> currentContext = chat.getContext();
93102

103+
ContentRenderer renderer = new ContentRenderer(parentPanel);
94104
for (ChatMessage chatMessage : currentContext) {
95105
if (chatMessage.getContent() != null) {
96-
ContentRenderer renderer = new ContentRenderer(parentPanel);
97106
JComponent messageComponent = renderer.render(chatMessage);
98107

99108
ChatMessageJPanel messageContainer = new ChatMessageJPanel(chatMessage);

0 commit comments

Comments
 (0)