feat: add Ollama icon and modern chat UI styling
- src/chat-view.ts: Add getIcon() returning 'bot' for the view tab icon.
Improve render() with role-specific CSS classes (user vs assistant) and
message header structure for better styling hooks.
- src/main.ts: Add ribbon icon ('bot') in the left sidebar that opens the
chat view with a single click.
- styles.css (new): Modern chat UI with message bubbles, distinct user and
assistant themes using Obsidian CSS variables, sticky input bar, styled
send button with accent color, and emoji role indicators.
- install.sh: Copy styles.css into the plugin directory and verify its
presence during installation.
- README.md: Include styles.css in manual install instructions.
- __mocks__/obsidian.ts: Add addRibbonIcon() mock for test compatibility.
- tests/chat-view.test.ts: Add getIcon() assertion.
This commit is contained in:
@@ -66,6 +66,7 @@ Then copy the plugin into your vault:
|
||||
mkdir -p /path/to/vault/.obsidian/plugins/ollama-plugin
|
||||
cp manifest.json /path/to/vault/.obsidian/plugins/ollama-plugin/
|
||||
cp main.js /path/to/vault/.obsidian/plugins/ollama-plugin/
|
||||
cp styles.css /path/to/vault/.obsidian/plugins/ollama-plugin/
|
||||
# Remove old dist/ from previous installs (no longer needed with bundling)
|
||||
rm -rf /path/to/vault/.obsidian/plugins/ollama-plugin/dist
|
||||
```
|
||||
|
||||
@@ -110,4 +110,8 @@ export class Plugin {
|
||||
constructor() {
|
||||
this.app = new App();
|
||||
}
|
||||
|
||||
addRibbonIcon(_icon: string, _title: string, _callback: () => void): HTMLElement {
|
||||
return document.createElement('div');
|
||||
}
|
||||
}
|
||||
|
||||
+4
-2
@@ -95,9 +95,10 @@ mkdir -p "$PLUGIN_DIR"
|
||||
# Remove old dist/ directory from previous installations (no longer needed with bundling)
|
||||
rm -rf "$PLUGIN_DIR/dist"
|
||||
|
||||
# Copy manifest and bundled entry point
|
||||
# Copy manifest, bundled entry point, and styles
|
||||
cp "$SCRIPT_DIR/manifest.json" "$PLUGIN_DIR/"
|
||||
cp "$SCRIPT_DIR/main.js" "$PLUGIN_DIR/"
|
||||
cp "$SCRIPT_DIR/styles.css" "$PLUGIN_DIR/"
|
||||
|
||||
# All dependencies are now bundled into main.js; no runtime node_modules needed.
|
||||
|
||||
@@ -107,9 +108,10 @@ info "Plugin installed to $PLUGIN_DIR"
|
||||
|
||||
step "Verifying installation"
|
||||
|
||||
if [ -f "$PLUGIN_DIR/manifest.json" ] && [ -f "$PLUGIN_DIR/main.js" ]; then
|
||||
if [ -f "$PLUGIN_DIR/manifest.json" ] && [ -f "$PLUGIN_DIR/main.js" ] && [ -f "$PLUGIN_DIR/styles.css" ]; then
|
||||
info "manifest.json ✓"
|
||||
info "main.js ✓"
|
||||
info "styles.css ✓"
|
||||
else
|
||||
error "Installation verification failed — missing files in $PLUGIN_DIR"
|
||||
exit 1
|
||||
|
||||
@@ -9005,6 +9005,9 @@ var ChatView = class extends import_obsidian3.ItemView {
|
||||
getDisplayText() {
|
||||
return "Ollama Chat";
|
||||
}
|
||||
getIcon() {
|
||||
return "bot";
|
||||
}
|
||||
async onOpen() {
|
||||
try {
|
||||
await this.ollamaClient.initializeCache();
|
||||
@@ -9059,9 +9062,12 @@ var ChatView = class extends import_obsidian3.ItemView {
|
||||
contentEl.textContent = msg.content;
|
||||
}
|
||||
} else {
|
||||
const messageEl = container.createEl("div", { cls: "ollama-message" });
|
||||
const messageEl = container.createEl("div", {
|
||||
cls: `ollama-message ollama-message-${msg.role}`
|
||||
});
|
||||
messageEl.setAttribute("data-msg-id", msg.id);
|
||||
messageEl.createEl("div", { cls: "ollama-message-role", text: msg.role });
|
||||
const headerEl = messageEl.createEl("div", { cls: "ollama-message-header" });
|
||||
headerEl.createEl("span", { cls: "ollama-message-role", text: msg.role });
|
||||
const contentEl = messageEl.createEl("div", { cls: "ollama-message-content" });
|
||||
contentEl.textContent = msg.content;
|
||||
}
|
||||
@@ -9307,7 +9313,10 @@ var ChatView = class extends import_obsidian3.ItemView {
|
||||
this.lastMessageEl = previousStreamingEl;
|
||||
}
|
||||
try {
|
||||
const entries = await this.vaultIndexer.searchVault(userMessage, this.settings.vaultSearchLimit);
|
||||
const entries = await this.vaultIndexer.searchVault(
|
||||
userMessage,
|
||||
this.settings.vaultSearchLimit
|
||||
);
|
||||
const context = entries.map((entry) => `${entry.title}
|
||||
${entry.content}`).join("\n\n").slice(0, MAX_CONTEXT_LENGTH);
|
||||
const userMessageWithContext = context ? `Relevant vault context:
|
||||
@@ -9402,6 +9411,9 @@ var OllamaPlugin = class extends import_obsidian4.Plugin {
|
||||
"ollama-chat-view",
|
||||
(leaf) => new ChatView(leaf, this.settings)
|
||||
);
|
||||
this.addRibbonIcon("bot", "Open Ollama Chat", async () => {
|
||||
await this.activateChatView();
|
||||
});
|
||||
this.addCommand({
|
||||
id: "open-ollama-chat",
|
||||
name: "Open Ollama Chat",
|
||||
|
||||
+17
-4
@@ -76,6 +76,10 @@ export class ChatView extends ItemView {
|
||||
return 'Ollama Chat';
|
||||
}
|
||||
|
||||
getIcon(): string {
|
||||
return 'bot';
|
||||
}
|
||||
|
||||
async onOpen(): Promise<void> {
|
||||
try {
|
||||
await this.ollamaClient.initializeCache();
|
||||
@@ -145,9 +149,14 @@ export class ChatView extends ItemView {
|
||||
contentEl.textContent = msg.content;
|
||||
}
|
||||
} else {
|
||||
const messageEl = container.createEl('div', { cls: 'ollama-message' });
|
||||
const messageEl = container.createEl('div', {
|
||||
cls: `ollama-message ollama-message-${msg.role}`,
|
||||
});
|
||||
messageEl.setAttribute('data-msg-id', msg.id);
|
||||
messageEl.createEl('div', { cls: 'ollama-message-role', text: msg.role });
|
||||
|
||||
const headerEl = messageEl.createEl('div', { cls: 'ollama-message-header' });
|
||||
headerEl.createEl('span', { cls: 'ollama-message-role', text: msg.role });
|
||||
|
||||
const contentEl = messageEl.createEl('div', { cls: 'ollama-message-content' });
|
||||
contentEl.textContent = msg.content;
|
||||
}
|
||||
@@ -449,7 +458,10 @@ export class ChatView extends ItemView {
|
||||
}
|
||||
|
||||
try {
|
||||
const entries = await this.vaultIndexer.searchVault(userMessage, this.settings.vaultSearchLimit);
|
||||
const entries = await this.vaultIndexer.searchVault(
|
||||
userMessage,
|
||||
this.settings.vaultSearchLimit
|
||||
);
|
||||
const context = entries
|
||||
.map((entry) => `${entry.title}\n${entry.content}`)
|
||||
.join('\n\n')
|
||||
@@ -459,7 +471,8 @@ export class ChatView extends ItemView {
|
||||
: userMessage;
|
||||
|
||||
// Get the complete messages array for the LLM with all context layers
|
||||
const completeMessages = this.conversationStateManager.getCompleteMessages(userMessageWithContext);
|
||||
const completeMessages =
|
||||
this.conversationStateManager.getCompleteMessages(userMessageWithContext);
|
||||
|
||||
const stream = this.ollamaClient.streamChat(completeMessages, tools);
|
||||
|
||||
|
||||
@@ -17,6 +17,11 @@ export default class OllamaPlugin extends Plugin {
|
||||
(leaf: WorkspaceLeaf) => new ChatView(leaf, this.settings)
|
||||
);
|
||||
|
||||
// Add a ribbon icon in the left sidebar
|
||||
this.addRibbonIcon('bot', 'Open Ollama Chat', async () => {
|
||||
await this.activateChatView();
|
||||
});
|
||||
|
||||
// Add a command to open the chat view
|
||||
this.addCommand({
|
||||
id: 'open-ollama-chat',
|
||||
|
||||
+151
@@ -0,0 +1,151 @@
|
||||
:root {
|
||||
--ollama-user-bg: var(--background-modifier-form-field);
|
||||
--ollama-assistant-bg: var(--background-primary-alt);
|
||||
--ollama-border: var(--background-modifier-border);
|
||||
--ollama-radius: var(--radius-m);
|
||||
--ollama-gap: var(--size-4-2);
|
||||
}
|
||||
|
||||
.ollama-chat-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ollama-gap);
|
||||
padding: var(--size-4-2);
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.ollama-message {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--size-4-1);
|
||||
padding: var(--size-4-2);
|
||||
border-radius: var(--ollama-radius);
|
||||
border: 1px solid var(--ollama-border);
|
||||
max-width: 90%;
|
||||
}
|
||||
|
||||
.ollama-message-user {
|
||||
align-self: flex-end;
|
||||
background-color: var(--ollama-user-bg);
|
||||
}
|
||||
|
||||
.ollama-message-assistant {
|
||||
align-self: flex-start;
|
||||
background-color: var(--ollama-assistant-bg);
|
||||
}
|
||||
|
||||
.ollama-message-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--size-4-1);
|
||||
font-size: var(--font-smallest);
|
||||
font-weight: var(--font-semibold);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.ollama-message-role::before {
|
||||
display: inline-block;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
margin-right: var(--size-4-1);
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.ollama-message-user .ollama-message-role::before {
|
||||
content: '👤';
|
||||
}
|
||||
|
||||
.ollama-message-assistant .ollama-message-role::before {
|
||||
content: '🤖';
|
||||
}
|
||||
|
||||
.ollama-message-content {
|
||||
line-height: var(--line-height-normal);
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.ollama-new-chat-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: var(--size-4-1) var(--size-4-2);
|
||||
}
|
||||
|
||||
.ollama-new-chat-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--size-4-1);
|
||||
padding: var(--size-4-1) var(--size-4-2);
|
||||
font-size: var(--font-smallest);
|
||||
border-radius: var(--ollama-radius);
|
||||
border: 1px solid var(--ollama-border);
|
||||
background-color: var(--background-modifier-form-field);
|
||||
color: var(--text-normal);
|
||||
cursor: pointer;
|
||||
transition: background-color 0.15s ease;
|
||||
}
|
||||
|
||||
.ollama-new-chat-button:hover {
|
||||
background-color: var(--background-modifier-hover);
|
||||
}
|
||||
|
||||
.ollama-new-chat-button::before {
|
||||
content: '➕';
|
||||
}
|
||||
|
||||
.ollama-input-container {
|
||||
display: flex;
|
||||
gap: var(--size-4-1);
|
||||
padding: var(--size-4-2);
|
||||
border-top: 1px solid var(--ollama-border);
|
||||
background-color: var(--background-primary);
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.ollama-input {
|
||||
flex: 1;
|
||||
min-height: 2.5rem;
|
||||
max-height: 8rem;
|
||||
padding: var(--size-4-1) var(--size-4-2);
|
||||
border-radius: var(--ollama-radius);
|
||||
border: 1px solid var(--ollama-border);
|
||||
background-color: var(--background-modifier-form-field);
|
||||
color: var(--text-normal);
|
||||
resize: vertical;
|
||||
font-family: inherit;
|
||||
font-size: var(--font-ui-small);
|
||||
line-height: var(--line-height-normal);
|
||||
}
|
||||
|
||||
.ollama-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--interactive-accent);
|
||||
box-shadow: 0 0 0 2px var(--interactive-accent-translucent);
|
||||
}
|
||||
|
||||
.ollama-send-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: var(--size-4-1) var(--size-4-3);
|
||||
border-radius: var(--ollama-radius);
|
||||
border: none;
|
||||
background-color: var(--interactive-accent);
|
||||
color: var(--text-on-accent);
|
||||
font-weight: var(--font-semibold);
|
||||
font-size: var(--font-ui-small);
|
||||
cursor: pointer;
|
||||
transition: filter 0.15s ease;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.ollama-send-button:hover {
|
||||
filter: brightness(1.1);
|
||||
}
|
||||
|
||||
.ollama-send-button:active {
|
||||
filter: brightness(0.95);
|
||||
}
|
||||
@@ -100,6 +100,12 @@ describe('ChatView', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('getIcon', () => {
|
||||
it('should return the bot icon', () => {
|
||||
expect(view.getIcon()).toBe('bot');
|
||||
});
|
||||
});
|
||||
|
||||
describe('onOpen', () => {
|
||||
it('should call render and setup event listeners', async () => {
|
||||
const renderSpy = jest.spyOn(view, 'render');
|
||||
|
||||
Reference in New Issue
Block a user