(8);
31 | builder2.AddAttribute(9, "ChildContent", (RenderFragment)(builder3 =>
32 | {
33 | builder3.AddMarkupContent(10, "Sorry, there\'s nothing at this address.
");
34 | }
35 | ));
36 | builder2.CloseComponent();
37 | }
38 | ));
39 | builder.CloseComponent();
40 | }
41 | }
42 | }
--------------------------------------------------------------------------------
/src/client-opensilver/OllamaHub.Support/Themes/Generic.xaml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/src/client-wpf/OllamaHub.Support/UI/Units/ModelStatusBadge.cs:
--------------------------------------------------------------------------------
1 | using System.Windows;
2 | using System.Windows.Controls;
3 |
4 | namespace OllamaHub.Support.UI.Units;
5 |
6 | public class ModelStatusBadge : Control
7 | {
8 | public ModelStatusBadge()
9 | {
10 | DefaultStyleKey = typeof(ModelStatusBadge);
11 | }
12 |
13 | public static readonly DependencyProperty CornerRadiusProperty =
14 | DependencyProperty.Register(
15 | nameof(CornerRadius),
16 | typeof(CornerRadius),
17 | typeof(ModelStatusBadge),
18 | new PropertyMetadata(new CornerRadius(0)));
19 |
20 | public static readonly DependencyProperty StatusProperty =
21 | DependencyProperty.Register(
22 | nameof(Status),
23 | typeof(ModelStatus),
24 | typeof(ModelStatusBadge),
25 | new PropertyMetadata(ModelStatus.Stopped, OnStatusChanged));
26 |
27 | public CornerRadius CornerRadius
28 | {
29 | get { return (CornerRadius)GetValue(CornerRadiusProperty); }
30 | set { SetValue(CornerRadiusProperty, value); }
31 | }
32 |
33 | public ModelStatus Status
34 | {
35 | get => (ModelStatus)GetValue(StatusProperty);
36 | set => SetValue(StatusProperty, value);
37 | }
38 |
39 | private static void OnStatusChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
40 | {
41 | if (d is ModelStatusBadge badge)
42 | {
43 | badge.UpdateVisualState();
44 | }
45 | }
46 |
47 | private void UpdateVisualState()
48 | {
49 | var state = Status.ToString();
50 | VisualStateManager.GoToState(this, state, true);
51 | }
52 |
53 | public override void OnApplyTemplate()
54 | {
55 | base.OnApplyTemplate();
56 | UpdateVisualState();
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/client-opensilver/OllamaHub.Support/UI/Units/ModelStatusBadge.cs:
--------------------------------------------------------------------------------
1 | using System.Windows;
2 | using System.Windows.Controls;
3 |
4 | namespace OllamaHub.Support.UI.Units;
5 |
6 | public class ModelStatusBadge : Control
7 | {
8 | public ModelStatusBadge()
9 | {
10 | DefaultStyleKey = typeof(ModelStatusBadge);
11 | }
12 |
13 | public static readonly DependencyProperty CornerRadiusProperty =
14 | DependencyProperty.Register(
15 | nameof(CornerRadius),
16 | typeof(CornerRadius),
17 | typeof(ModelStatusBadge),
18 | new PropertyMetadata(new CornerRadius(0)));
19 |
20 | public static readonly DependencyProperty StatusProperty =
21 | DependencyProperty.Register(
22 | nameof(Status),
23 | typeof(ModelStatus),
24 | typeof(ModelStatusBadge),
25 | new PropertyMetadata(ModelStatus.Stopped, OnStatusChanged));
26 |
27 | public CornerRadius CornerRadius
28 | {
29 | get { return (CornerRadius)GetValue(CornerRadiusProperty); }
30 | set { SetValue(CornerRadiusProperty, value); }
31 | }
32 |
33 | public ModelStatus Status
34 | {
35 | get => (ModelStatus)GetValue(StatusProperty);
36 | set => SetValue(StatusProperty, value);
37 | }
38 |
39 | private static void OnStatusChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
40 | {
41 | if (d is ModelStatusBadge badge)
42 | {
43 | badge.UpdateVisualState();
44 | }
45 | }
46 |
47 | private void UpdateVisualState()
48 | {
49 | var state = Status.ToString();
50 | VisualStateManager.GoToState(this, state, true);
51 | }
52 |
53 | public override void OnApplyTemplate()
54 | {
55 | base.OnApplyTemplate();
56 | UpdateVisualState();
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/client-wpf/OllamaHub.Support/Themes/Units/ModelListBox.xaml:
--------------------------------------------------------------------------------
1 |
5 |
6 |
38 |
39 |
--------------------------------------------------------------------------------
/src/client-opensilver/OllamaHub.Support/Themes/Units/ModelListBox.xaml:
--------------------------------------------------------------------------------
1 |
5 |
6 |
38 |
39 |
--------------------------------------------------------------------------------
/src/client-opensilver/OllamaHub.Browser/wwwroot/modern/loading-indicator.css:
--------------------------------------------------------------------------------
1 | html body {
2 | background: var(--opensilver-loading-background-color);
3 | }
4 |
5 | .opensilver-loading-indicator {
6 | position: fixed;
7 | top: 0;
8 | left: 0;
9 | display: flex;
10 | justify-content: center;
11 | align-items: center;
12 | pointer-events: none;
13 | height: 100vh;
14 | width: 100vw;
15 | overflow: hidden;
16 | background: var(--opensilver-loading-background-color);
17 | }
18 |
19 | .opensilver-loading-indicator .opensilver-loader-container {
20 | display: flex;
21 | justify-content: center;
22 | align-items: center;
23 | gap: 8px;
24 | }
25 |
26 | .opensilver-loading-indicator .opensilver-loader {
27 | display: flex;
28 | flex-direction: column;
29 | justify-content: center;
30 | align-items: center;
31 | gap: 10px;
32 | width: 100%;
33 | height: 100%;
34 | margin-left: 20px;
35 | }
36 |
37 | .opensilver-loading-indicator .opensilver-loader-progress {
38 | display: flex;
39 | justify-content: flex-start;
40 | align-items: center;
41 | width: 0;
42 | max-width: 200px;
43 | height: 4px;
44 | border-radius: 12px;
45 | background-color: var(--opensilver-loading-progress-bg);
46 | border-bottom: 1px var(--opensilver-loading-progress-border-color) solid;
47 | border-right: 1px var(--opensilver-loading-progress-border-color) solid;
48 | }
49 |
50 | .opensilver-loading-indicator .opensilver-loader-progress-bar {
51 | width: 0%;
52 | height: 5px;
53 | border-radius: 12px;
54 | background-color: var(--opensilver-loading-progress-bar-color);
55 | }
56 |
57 | .opensilver-loading-indicator .opensilver-counter-container {
58 | align-self: end;
59 | display: flex;
60 | justify-content: end;
61 | align-items: center;
62 | font-family: sans-serif;
63 | font-size: 0.8rem;
64 | font-weight: 500;
65 | width: 100%;
66 | min-width: 26px;
67 | line-height: 1;
68 | gap: 1px;
69 | color: var(--opensilver-loading-counter-color);
70 | }
71 |
--------------------------------------------------------------------------------
/src/OllamaWPF.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 17
4 | VisualStudioVersion = 17.14.36221.1
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OllamaHub", "client-wpf\OllamaHub\OllamaHub.csproj", "{AB0CFB89-1BEB-97B9-18D6-428E585FEDB0}"
7 | EndProject
8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OllamaHub.Main", "client-wpf\OllamaHub.Main\OllamaHub.Main.csproj", "{9600DDE8-A452-2E7D-C30B-17F3402727D3}"
9 | EndProject
10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OllamaHub.Support", "client-wpf\OllamaHub.Support\OllamaHub.Support.csproj", "{94E02CA6-4496-878D-F6C2-B8AEAF9D4F9C}"
11 | EndProject
12 | Global
13 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
14 | Debug|Any CPU = Debug|Any CPU
15 | Release|Any CPU = Release|Any CPU
16 | EndGlobalSection
17 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
18 | {AB0CFB89-1BEB-97B9-18D6-428E585FEDB0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
19 | {AB0CFB89-1BEB-97B9-18D6-428E585FEDB0}.Debug|Any CPU.Build.0 = Debug|Any CPU
20 | {AB0CFB89-1BEB-97B9-18D6-428E585FEDB0}.Release|Any CPU.ActiveCfg = Release|Any CPU
21 | {AB0CFB89-1BEB-97B9-18D6-428E585FEDB0}.Release|Any CPU.Build.0 = Release|Any CPU
22 | {9600DDE8-A452-2E7D-C30B-17F3402727D3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
23 | {9600DDE8-A452-2E7D-C30B-17F3402727D3}.Debug|Any CPU.Build.0 = Debug|Any CPU
24 | {9600DDE8-A452-2E7D-C30B-17F3402727D3}.Release|Any CPU.ActiveCfg = Release|Any CPU
25 | {9600DDE8-A452-2E7D-C30B-17F3402727D3}.Release|Any CPU.Build.0 = Release|Any CPU
26 | {94E02CA6-4496-878D-F6C2-B8AEAF9D4F9C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
27 | {94E02CA6-4496-878D-F6C2-B8AEAF9D4F9C}.Debug|Any CPU.Build.0 = Debug|Any CPU
28 | {94E02CA6-4496-878D-F6C2-B8AEAF9D4F9C}.Release|Any CPU.ActiveCfg = Release|Any CPU
29 | {94E02CA6-4496-878D-F6C2-B8AEAF9D4F9C}.Release|Any CPU.Build.0 = Release|Any CPU
30 | EndGlobalSection
31 | GlobalSection(SolutionProperties) = preSolution
32 | HideSolutionNode = FALSE
33 | EndGlobalSection
34 | GlobalSection(ExtensibilityGlobals) = postSolution
35 | SolutionGuid = {E5FEB065-1766-4791-B8A6-3467CA24D472}
36 | EndGlobalSection
37 | EndGlobal
38 |
--------------------------------------------------------------------------------
/src/client-wpf/OllamaHub.Support/Themes/Units/UserMessageListBoxItem.xaml:
--------------------------------------------------------------------------------
1 |
5 |
6 |
41 |
42 |
--------------------------------------------------------------------------------
/src/client-opensilver/OllamaHub.Support/Themes/Units/UserMessageListBoxItem.xaml:
--------------------------------------------------------------------------------
1 |
5 |
6 |
41 |
42 |
--------------------------------------------------------------------------------
/src/client-opensilver/OllamaHub.Support/Local/Services/ApiClient.cs:
--------------------------------------------------------------------------------
1 | using OllamaHub.Support.Local.Models;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.Net.Http;
5 | using System.Text;
6 | using System.Text.Json;
7 | using System.Threading.Tasks;
8 |
9 | namespace OllamaHub.Support.Local.Services;
10 |
11 | public class ApiClient : IDisposable
12 | {
13 | private readonly HttpClient _http;
14 | private readonly JsonSerializerOptions _jsonOptions;
15 |
16 | public ApiClient(string baseUrl)
17 | {
18 | _http = new HttpClient
19 | {
20 | BaseAddress = new Uri(baseUrl),
21 | Timeout = TimeSpan.FromMinutes(5)
22 | };
23 |
24 | _jsonOptions = new JsonSerializerOptions
25 | {
26 | PropertyNameCaseInsensitive = true
27 | };
28 | }
29 |
30 | public async Task> GetAsync(string endpoint)
31 | {
32 | try
33 | {
34 | var response = await _http.GetAsync(endpoint);
35 | response.EnsureSuccessStatusCode();
36 |
37 | var json = await response.Content.ReadAsStringAsync();
38 | var apiResponse = JsonSerializer.Deserialize>>(json, _jsonOptions);
39 | return apiResponse?.Models ?? new List();
40 | }
41 | catch (HttpRequestException ex)
42 | {
43 | Console.WriteLine($"API GET 오류: {ex.Message}");
44 | return new List();
45 | }
46 | }
47 |
48 | public async Task PostAsync(string endpoint, object data = null)
49 | {
50 | try
51 | {
52 | HttpContent content = null;
53 | if (data != null)
54 | {
55 | var json = JsonSerializer.Serialize(data);
56 | content = new StringContent(json, Encoding.UTF8, "application/json");
57 | }
58 |
59 | var response = await _http.PostAsync(endpoint, content);
60 | response.EnsureSuccessStatusCode();
61 |
62 | return await response.Content.ReadAsStringAsync();
63 | }
64 | catch (HttpRequestException ex)
65 | {
66 | Console.WriteLine($"API POST 오류: {ex.Message}");
67 | return string.Empty;
68 | }
69 | }
70 |
71 | public void Dispose()
72 | {
73 | _http?.Dispose();
74 | }
75 | }
--------------------------------------------------------------------------------
/src/client-opensilver/OllamaHub.Browser/wwwroot/libs/FileSaver.min.js:
--------------------------------------------------------------------------------
1 | /* FileSaver.js 2.0.5, @license MIT */
2 | (function(a,b){if("function"==typeof define&&define.amd)define([],b);else if("undefined"!=typeof exports)b();else{b(),a.FileSaver={exports:{}}.exports}})(this,function(){"use strict";function b(a,b){return"undefined"==typeof b?b={autoBom:!1}:"object"!=typeof b&&(console.warn("Deprecated: Expected third argument to be a object"),b={autoBom:!b}),b.autoBom&&/^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(a.type)?new Blob(["\uFEFF",a],{type:a.type}):a}function c(a,b,c){var d=new XMLHttpRequest;d.open("GET",a),d.responseType="blob",d.onload=function(){g(d.response,b,c)},d.onerror=function(){console.error("could not download file")},d.send()}function d(a){var b=new XMLHttpRequest;b.open("HEAD",a,!1);try{b.send()}catch(a){}return 200<=b.status&&299>=b.status}function e(a){try{a.dispatchEvent(new MouseEvent("click"))}catch(c){var b=document.createEvent("MouseEvents");b.initMouseEvent("click",!0,!0,window,0,0,0,80,20,!1,!1,!1,!1,0,null),a.dispatchEvent(b)}}var f="object"==typeof window&&window.window===window?window:"object"==typeof self&&self.self===self?self:"object"==typeof global&&global.global===global?global:void 0,a=f.navigator&&/Macintosh/.test(navigator.userAgent)&&/AppleWebKit/.test(navigator.userAgent)&&!/Safari/.test(navigator.userAgent),g=f.saveAs||("object"!=typeof window||window!==f?function(){}:"download"in HTMLAnchorElement.prototype&&!a?function(b,g,h){var i=f.URL||f.webkitURL,j=document.createElement("a");g=g||b.name||"download",j.download=g,j.rel="noopener","string"==typeof b?(j.href=b,j.origin===location.origin?e(j):d(j.href)?c(b,g,h):e(j,j.target="_blank")):(j.href=i.createObjectURL(b),setTimeout(function(){i.revokeObjectURL(j.href)},4E4),setTimeout(function(){e(j)},0))}:"msSaveOrOpenBlob"in navigator?function(f,g,h){if(g=g||f.name||"download","string"!=typeof f)navigator.msSaveOrOpenBlob(b(f,h),g);else if(d(f))c(f,g,h);else{var i=document.createElement("a");i.href=f,i.target="_blank",setTimeout(function(){e(i)})}}:function(b,d,e,g){if(g=g||open("","_blank"),g&&(g.document.title=g.document.body.innerText="downloading..."),"string"==typeof b)return c(b,d,e);var h="application/octet-stream"===b.type,i=/constructor/i.test(f.HTMLElement)||f.safari,j=/CriOS\/[\d]+/.test(navigator.userAgent);if((j||h&&i||a)&&"undefined"!=typeof FileReader){var k=new FileReader;k.onloadend=function(){var a=k.result;a=j?a:a.replace(/^data:[^;]*;/,"data:attachment/file;"),g?g.location.href=a:location=a,g=null},k.readAsDataURL(b)}else{var l=f.URL||f.webkitURL,m=l.createObjectURL(b);g?g.location=m:location.href=m,g=null,setTimeout(function(){l.revokeObjectURL(m)},4E4)}});f.saveAs=g.saveAs=g,"undefined"!=typeof module&&(module.exports=g)});
3 |
4 | //# sourceMappingURL=FileSaver.min.js.map
--------------------------------------------------------------------------------
/src/client-wpf/OllamaHub.Support/UI/Units/ChatListBox.cs:
--------------------------------------------------------------------------------
1 | using OllamaHub.Support.Local.Models;
2 | using System.Collections.Specialized;
3 | using System.Windows;
4 | using System.Windows.Controls;
5 |
6 | namespace OllamaHub.Support.UI.Units;
7 |
8 | public class ChatListBox : ListBox
9 | {
10 | private int _currentItemIndex = -1; // 생성 중인 아이템 인덱스 추적
11 |
12 | public ChatListBox()
13 | {
14 | DefaultStyleKey = typeof(ChatListBox);
15 | _currentItemIndex = -1;
16 |
17 | ListBoxAutoScrollBehavior.SetAutoScroll (this, true);
18 | }
19 |
20 | protected override DependencyObject GetContainerForItemOverride()
21 | {
22 | if (Items.Count > 0)
23 | {
24 | var currentItem = Items[Items.Count - 1];
25 | if (currentItem is UserMessage)
26 | {
27 | return new UserMessageListBoxItem();
28 | }
29 | else if (currentItem is AIMessage)
30 | {
31 | return new AIMessageListBoxItem();
32 | }
33 | }
34 | return base.GetContainerForItemOverride();
35 | }
36 |
37 | protected override void OnItemsChanged(NotifyCollectionChangedEventArgs e)
38 | {
39 | base.OnItemsChanged(e);
40 | _currentItemIndex = -1;
41 | }
42 | }
43 |
44 | public static class ListBoxAutoScrollBehavior
45 | {
46 | public static readonly DependencyProperty AutoScrollProperty =
47 | DependencyProperty.RegisterAttached (
48 | "AutoScroll",
49 | typeof (bool),
50 | typeof (ListBoxAutoScrollBehavior),
51 | new PropertyMetadata (false, OnAutoScrollChanged));
52 |
53 | public static bool GetAutoScroll(DependencyObject obj) =>
54 | (bool)obj.GetValue (AutoScrollProperty);
55 |
56 | public static void SetAutoScroll(DependencyObject obj, bool value) =>
57 | obj.SetValue (AutoScrollProperty, value);
58 |
59 | private static void OnAutoScrollChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
60 | {
61 | if (d is ListBox listBox)
62 | {
63 | if ((bool)e.NewValue)
64 | {
65 | var items = listBox.Items;
66 | var ic = items as INotifyCollectionChanged;
67 | if (ic != null)
68 | {
69 | ic.CollectionChanged += (sender, args) =>
70 | {
71 | if (args.Action == NotifyCollectionChangedAction.Add)
72 | {
73 | if (listBox.Items.Count > 0)
74 | {
75 | listBox.ScrollIntoView (listBox.Items[listBox.Items.Count - 1]);
76 | }
77 | }
78 | };
79 | }
80 | }
81 | }
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/src/client-opensilver/OllamaHub.Browser/wwwroot/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | OllamaHub
7 |
8 |
17 |
31 |
32 |
33 |
34 |
35 |
36 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
65 |
66 |
--------------------------------------------------------------------------------
/README_ko.md:
--------------------------------------------------------------------------------
1 | # Ollama Manager
2 |
3 | OpenSilver로 개발된 Ollama 모델 관리 웹 애플리케이션입니다.
4 |
5 | OpenSilver 버전을 WPF로 마이그레이션한 데스크톱 버전도 함께 제공하여 두 플랫폼 간의 개발 경험을 비교하고 학습할 수 있도록 구성했습니다.
6 |
7 | ## 목차
8 |
9 | - [스크린샷](#스크린샷)
10 | - [주요 기능](#주요-기능)
11 | - [프로젝트 구조](#프로젝트-구조)
12 | - [기술 스택](#기술-스택)
13 | - [서버 아키텍처](#서버-아키텍처)
14 | - [실행 방법](#실행-방법)
15 | - [개발 계획](#개발-계획)
16 | - [기여하기](#기여하기)
17 | - [라이센스](#라이센스)
18 | - [참고사항](#참고사항)
19 |
20 | ## 스크린샷
21 |
22 | 직관적인 인터페이스로 Ollama 모델을 쉽게 관리하고 실시간으로 채팅할 수 있습니다.
23 |
24 | | 메인 화면 | 채팅 화면 |
25 | |-----------|-----------|
26 | |  |  |
27 |
28 | ## 주요 기능
29 |
30 | - 설치된 모델 목록 조회
31 | - 모델 시작/중지
32 | - 실시간 채팅
33 | - 모델 상태 모니터링
34 |
35 | ## 프로젝트 구조
36 |
37 | ```
38 | src/
39 | ├── client-opensilver/ # OpenSilver 웹 클라이언트
40 | ├── client-wpf/ # WPF 데스크톱 클라이언트
41 | └── server-minimalapi/ # 공유 백엔드 서버
42 | ```
43 |
44 | ## 기술 스택
45 |
46 | - **웹 클라이언트**: OpenSilver (.NET Standard 2.0)
47 | - **데스크톱 클라이언트**: WPF (.NET 9.0)
48 | - **백엔드**: ASP.NET Core Minimal API (.NET 9.0)
49 | - **실시간 통신**: SignalR
50 |
51 | ## 서버 아키텍처
52 |
53 | ### Minimal API 구조
54 | - **GET** `/api/models` - 설치된 모델 목록 및 상태 조회
55 | - **POST** `/api/models/{modelName}/start` - 모델 시작
56 | - **POST** `/api/models/{modelName}/stop` - 모델 중지
57 | - **POST** `/api/chat` - 모델과 채팅
58 |
59 | ### Ollama API 연동
60 | 백엔드 서버는 Ollama API를 활용하여 모델을 관리합니다:
61 | - `http://localhost:11434/api/tags` - 설치된 모델 목록
62 | - `http://localhost:11434/api/ps` - 실행 중인 모델 확인
63 | - `http://localhost:11434/api/generate` - 모델 로드/언로드 및 채팅
64 |
65 | ### 실시간 모니터링
66 | - SignalR Hub를 통한 실시간 모델 상태 업데이트
67 | - 백그라운드 서비스로 모델 상태 변화 감지
68 |
69 | ## 실행 방법
70 |
71 | ### 사전 요구사항
72 | - **Ollama**: [ollama.com](https://ollama.com)에서 설치
73 | - **모델 설치**: 예시로 `ollama pull llama3.2` 실행
74 | - **Visual Studio 2022**
75 | - **.NET 9.0 SDK**
76 | - **WASM Tools**: `dotnet workload install wasm-tools`
77 | - **OpenSilver SDK**: [www.opensilver.net](https://www.opensilver.net)에서 `OpenSilver_SDK_v3.2.0.4.vsix` 다운로드 후 설치
78 |
79 | ### 서버 실행
80 |
81 | 먼저 백엔드 서버를 실행합니다:
82 | ```bash
83 | cd src/server-minimalapi/LocalLLMServer
84 | dotnet run --launch-profile https
85 | ```
86 | 서버가 `https://localhost:7262`에서 실행됩니다.
87 |
88 | ### OpenSilver 웹 클라이언트
89 |
90 | ```bash
91 | cd src/client-opensilver/OllamaHub.Browser
92 | dotnet run
93 | ```
94 |
95 | 브라우저에서 `http://localhost:55592` 접속
96 |
97 | ### WPF 버전
98 |
99 | 동일한 서버를 사용하여 WPF 버전도 실행할 수 있습니다:
100 | ```bash
101 | cd src/client-wpf/OllamaHub
102 | dotnet run
103 | ```
104 |
105 | ## 개발 계획
106 |
107 | ### 현재 구현됨
108 | - 모델 목록 조회
109 | - 모델 제어 (시작/중지)
110 | - 실시간 채팅
111 |
112 | ### 예정 기능
113 | - 모델 다운로드
114 | - 모델 삭제
115 | - 다중 세션 채팅
116 | - 성능 모니터링
117 |
118 | ## 기여하기
119 |
120 | 1. 저장소 포크
121 | 2. 기능 브랜치 생성
122 | 3. 변경사항 커밋
123 | 4. Pull Request 생성
124 |
125 | ## 라이센스
126 |
127 | MIT License
128 |
129 | ## 참고사항
130 |
131 | - WPF와 OpenSilver 간의 코드 호환성을 확인할 수 있는 좋은 예제입니다
132 | - OpenSilver는 .NET Standard 2.0을 기반으로 하여 웹에서 WPF와 유사한 개발 경험을 제공합니다
133 |
--------------------------------------------------------------------------------
/src/client-wpf/OllamaHub.Main/Themes/Views/MainContent.xaml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
10 |
11 |
52 |
53 |
--------------------------------------------------------------------------------
/src/server-minimalapi/LocalLLMServer/Service/Background/ModelMonitorService.cs:
--------------------------------------------------------------------------------
1 | using LocalLLMServer.SignalRHub;
2 | using Microsoft.AspNetCore.SignalR;
3 | using System.Diagnostics;
4 |
5 | namespace LocalLLMServer.Service.Background;
6 | public class ModelMonitorService : BackgroundService
7 | {
8 | private readonly IHubContext _hubContext;
9 | private readonly Dictionary _lastStatus = new();
10 |
11 | public ModelMonitorService(IHubContext hubContext) => _hubContext = hubContext;
12 |
13 | protected override async Task ExecuteAsync(CancellationToken stoppingToken)
14 | {
15 | while (!stoppingToken.IsCancellationRequested)
16 | {
17 | var currentStatus = await GetModelStatus();
18 |
19 | foreach (var (modelName, status) in currentStatus)
20 | {
21 | if (!_lastStatus.TryGetValue(modelName, out var prevStatus) || prevStatus != status)
22 | {
23 | _lastStatus[modelName] = status;
24 | await _hubContext.Clients.Group("ModelUpdates").SendAsync("ModelStatusChanged", modelName, status);
25 | }
26 | }
27 |
28 | foreach (var removed in _lastStatus.Keys.Except(currentStatus.Keys).ToList())
29 | _lastStatus.Remove(removed);
30 |
31 | await Task.Delay(3000, stoppingToken);
32 | }
33 | }
34 |
35 | private async Task> GetModelStatus()
36 | {
37 | var result = new Dictionary();
38 | var running = new HashSet();
39 |
40 | var psProcess = new Process
41 | {
42 | StartInfo = new ProcessStartInfo
43 | {
44 | FileName = "ollama",
45 | Arguments = "ps",
46 | RedirectStandardOutput = true,
47 | UseShellExecute = false,
48 | CreateNoWindow = true
49 | }
50 | };
51 | psProcess.Start();
52 | var psOutput = await psProcess.StandardOutput.ReadToEndAsync();
53 | await psProcess.WaitForExitAsync();
54 |
55 | foreach (var line in psOutput.Split('\n').Skip(1))
56 | {
57 | var parts = line.Trim().Split(' ', StringSplitOptions.RemoveEmptyEntries);
58 | if (parts.Length > 0) running.Add(parts[0]);
59 | }
60 |
61 | var listProcess = new Process
62 | {
63 | StartInfo = new ProcessStartInfo
64 | {
65 | FileName = "ollama",
66 | Arguments = "list",
67 | RedirectStandardOutput = true,
68 | UseShellExecute = false,
69 | CreateNoWindow = true
70 | }
71 | };
72 | listProcess.Start();
73 | var listOutput = await listProcess.StandardOutput.ReadToEndAsync();
74 | await listProcess.WaitForExitAsync();
75 |
76 | foreach (var line in listOutput.Split('\n').Skip(1))
77 | {
78 | var parts = line.Trim().Split(' ', StringSplitOptions.RemoveEmptyEntries);
79 | if (parts.Length >= 1)
80 | {
81 | var modelName = parts[0];
82 | result[modelName] = running.Contains(modelName) ? "Running" : "Stopped";
83 | }
84 | }
85 |
86 | return result;
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/src/client-opensilver/OllamaHub.Main/Themes/Views/MainContent.xaml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
10 |
11 |
56 |
--------------------------------------------------------------------------------
/src/client-opensilver/OllamaHub.Support/Themes/Units/ChatListBox.xaml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 |
60 |
61 |
--------------------------------------------------------------------------------
/src/OllamaOpenSilver.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 17
4 | VisualStudioVersion = 17.13.35919.96
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OllamaHub", "client-opensilver\OllamaHub\OllamaHub.csproj", "{6D7DC9D8-127D-A873-B5DC-67DDA6A9E73E}"
7 | EndProject
8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OllamaHub.Browser", "client-opensilver\OllamaHub.Browser\OllamaHub.Browser.csproj", "{027ADC29-76E8-1B3F-3464-C54C0E9417C9}"
9 | EndProject
10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OllamaHub.Support", "client-opensilver\OllamaHub.Support\OllamaHub.Support.csproj", "{33586B4F-83EF-9A03-C6C7-4959C83D11BF}"
11 | EndProject
12 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "App", "App", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}"
13 | EndProject
14 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Build", "Build", "{ED3DE717-1672-4FD7-AD3D-EE8A681B78AB}"
15 | EndProject
16 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Based", "Based", "{FFF3EAF6-7F8A-4C6F-841C-C389C50A7FC7}"
17 | EndProject
18 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Partial", "Partial", "{CF04B1B3-6F0C-4513-8FE9-A05F8D9896E9}"
19 | EndProject
20 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OllamaHub.Main", "client-opensilver\OllamaHub.Main\OllamaHub.Main.csproj", "{4E08E2F1-C3D9-492C-9DC0-C23F8A17B691}"
21 | EndProject
22 | Global
23 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
24 | Debug|Any CPU = Debug|Any CPU
25 | Release|Any CPU = Release|Any CPU
26 | EndGlobalSection
27 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
28 | {6D7DC9D8-127D-A873-B5DC-67DDA6A9E73E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
29 | {6D7DC9D8-127D-A873-B5DC-67DDA6A9E73E}.Debug|Any CPU.Build.0 = Debug|Any CPU
30 | {6D7DC9D8-127D-A873-B5DC-67DDA6A9E73E}.Release|Any CPU.ActiveCfg = Release|Any CPU
31 | {6D7DC9D8-127D-A873-B5DC-67DDA6A9E73E}.Release|Any CPU.Build.0 = Release|Any CPU
32 | {027ADC29-76E8-1B3F-3464-C54C0E9417C9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
33 | {027ADC29-76E8-1B3F-3464-C54C0E9417C9}.Debug|Any CPU.Build.0 = Debug|Any CPU
34 | {027ADC29-76E8-1B3F-3464-C54C0E9417C9}.Release|Any CPU.ActiveCfg = Release|Any CPU
35 | {027ADC29-76E8-1B3F-3464-C54C0E9417C9}.Release|Any CPU.Build.0 = Release|Any CPU
36 | {33586B4F-83EF-9A03-C6C7-4959C83D11BF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
37 | {33586B4F-83EF-9A03-C6C7-4959C83D11BF}.Debug|Any CPU.Build.0 = Debug|Any CPU
38 | {33586B4F-83EF-9A03-C6C7-4959C83D11BF}.Release|Any CPU.ActiveCfg = Release|Any CPU
39 | {33586B4F-83EF-9A03-C6C7-4959C83D11BF}.Release|Any CPU.Build.0 = Release|Any CPU
40 | {4E08E2F1-C3D9-492C-9DC0-C23F8A17B691}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
41 | {4E08E2F1-C3D9-492C-9DC0-C23F8A17B691}.Debug|Any CPU.Build.0 = Debug|Any CPU
42 | {4E08E2F1-C3D9-492C-9DC0-C23F8A17B691}.Release|Any CPU.ActiveCfg = Release|Any CPU
43 | {4E08E2F1-C3D9-492C-9DC0-C23F8A17B691}.Release|Any CPU.Build.0 = Release|Any CPU
44 | EndGlobalSection
45 | GlobalSection(SolutionProperties) = preSolution
46 | HideSolutionNode = FALSE
47 | EndGlobalSection
48 | GlobalSection(NestedProjects) = preSolution
49 | {6D7DC9D8-127D-A873-B5DC-67DDA6A9E73E} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
50 | {027ADC29-76E8-1B3F-3464-C54C0E9417C9} = {ED3DE717-1672-4FD7-AD3D-EE8A681B78AB}
51 | {33586B4F-83EF-9A03-C6C7-4959C83D11BF} = {FFF3EAF6-7F8A-4C6F-841C-C389C50A7FC7}
52 | {4E08E2F1-C3D9-492C-9DC0-C23F8A17B691} = {CF04B1B3-6F0C-4513-8FE9-A05F8D9896E9}
53 | EndGlobalSection
54 | GlobalSection(ExtensibilityGlobals) = postSolution
55 | SolutionGuid = {8842B7F8-0001-4090-A300-290B774D96BB}
56 | EndGlobalSection
57 | EndGlobal
58 |
--------------------------------------------------------------------------------
/src/client-wpf/OllamaHub.Support/Themes/Units/ModelListBoxItem.xaml:
--------------------------------------------------------------------------------
1 |
5 |
6 |
62 |
63 |
--------------------------------------------------------------------------------
/src/client-opensilver/OllamaHub.Support/Themes/Units/ModelListBoxItem.xaml:
--------------------------------------------------------------------------------
1 |
5 |
6 |
62 |
63 |
--------------------------------------------------------------------------------
/src/client-opensilver/OllamaHub.Browser/wwwroot/loading-indicator.css:
--------------------------------------------------------------------------------
1 | @keyframes loading-indicator-ball-anim {
2 | 0% {
3 | transform: translate(-50%, -50%) scale(0);
4 | opacity: 0;
5 | }
6 |
7 | 25% {
8 | transform: translate(-50%, -50%) scale(1);
9 | opacity: 1;
10 | }
11 |
12 | 32% {
13 | transform: translate(-50%, -50%) scale(0.5);
14 | opacity: 0;
15 | }
16 |
17 | 100% {
18 | transform: translate(-50%, -50%) scale(0);
19 | opacity: 0;
20 | }
21 | }
22 |
23 | body {
24 | margin: 0;
25 | }
26 |
27 | .loading-indicator-wrapper {
28 | display: flex;
29 | justify-content: center;
30 | align-items: center;
31 | width: 100vw;
32 | height: 100vh;
33 | background-color: #f0f0f0; /* Lighter background */
34 | }
35 |
36 | .loading-indicator {
37 | position: relative;
38 | width: 80px;
39 | height: 80px;
40 | pointer-events: none;
41 | }
42 |
43 | .loading-indicator-ball {
44 | will-change: transform, opacity;
45 | position: absolute;
46 | width: 16%;
47 | height: 16%;
48 | border-radius: 50%;
49 | background: #3b8eea; /* Light blue */
50 | filter: blur(3px); /* Slightly lighter blur */
51 | opacity: 0;
52 | animation: loading-indicator-ball-anim 9s infinite;
53 | }
54 |
55 | .loading-indicator-ball:nth-child(1) {
56 | left: 85.3553390593%;
57 | top: 85.3553390593%;
58 | animation-delay: 0s;
59 | }
60 |
61 | .loading-indicator-ball:nth-child(2) {
62 | left: 100%;
63 | top: 50%;
64 | animation-delay: 0.2s;
65 | }
66 |
67 | .loading-indicator-ball:nth-child(3) {
68 | left: 85.3553390593%;
69 | top: 14.6446609407%;
70 | --rotation: calc(-45deg * 3);
71 | animation-delay: 0.4s;
72 | }
73 |
74 | .loading-indicator-ball:nth-child(4) {
75 | left: 50%;
76 | top: 0%;
77 | animation-delay: 0.6s;
78 | }
79 |
80 | .loading-indicator-ball:nth-child(5) {
81 | left: 14.6446609407%;
82 | top: 14.6446609407%;
83 | animation-delay: 0.8s;
84 | }
85 |
86 | .loading-indicator-ball:nth-child(6) {
87 | left: 0%;
88 | top: 50%;
89 | animation-delay: 1.0s;
90 | }
91 |
92 | .loading-indicator-ball:nth-child(7) {
93 | left: 14.6446609407%;
94 | top: 85.3553390593%;
95 | animation-delay: 1.2s;
96 | }
97 |
98 | .loading-indicator-ball:nth-child(8) {
99 | left: 50%;
100 | top: 100%;
101 | animation-delay: 1.4s;
102 | }
103 |
104 | .loading-indicator-ball:nth-child(9) {
105 | left: 50%;
106 | top: 100%;
107 | animation-delay: 4.5s;
108 | }
109 |
110 | .loading-indicator-ball:nth-child(10) {
111 | left: 14.6446609407%;
112 | top: 85.3553390593%;
113 | animation-delay: 4.7s;
114 | }
115 |
116 | .loading-indicator-ball:nth-child(11) {
117 | left: 0%;
118 | top: 50%;
119 | animation-delay: 4.9s;
120 | }
121 |
122 | .loading-indicator-ball:nth-child(12) {
123 | left: 14.6446609407%;
124 | top: 14.6446609407%;
125 | animation-delay: 5.1s;
126 | }
127 |
128 | .loading-indicator-ball:nth-child(13) {
129 | left: 50%;
130 | top: 0%;
131 | animation-delay: 5.3s;
132 | }
133 |
134 | .loading-indicator-ball:nth-child(14) {
135 | left: 85.3553390593%;
136 | top: 14.6446609407%;
137 | animation-delay: 5.5s;
138 | }
139 |
140 | .loading-indicator-ball:nth-child(15) {
141 | left: 100%;
142 | top: 50%;
143 | animation-delay: 5.7s;
144 | }
145 |
146 | .loading-indicator-ball:nth-child(16) {
147 | left: 85.3553390593%;
148 | top: 85.3553390593%;
149 | animation-delay: 5.9s;
150 | }
151 |
152 | .loading-indicator-text {
153 | display: flex;
154 | justify-content: center;
155 | align-items: center;
156 | width: 100%;
157 | height: 100%;
158 | }
159 |
160 | .loading-indicator-text:after {
161 | content: var(--blazor-load-percentage-text, "Loading...");
162 | color: #555; /* Darker text for contrast */
163 | font-size: 1.2rem;
164 | font-family: 'Arial', sans-serif;
165 | }
166 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Ollama Manager
2 |
3 | A web-based Ollama model management application developed with OpenSilver.
4 |
5 | This project also includes a WPF desktop version migrated from the OpenSilver version, allowing developers to compare and learn from the development experience across both platforms.
6 |
7 |
8 |
9 |
10 | ## Table of Contents
11 |
12 | - [Screenshots](#screenshots)
13 | - [Key Features](#key-features)
14 | - [Project Structure](#project-structure)
15 | - [Tech Stack](#tech-stack)
16 | - [Server Architecture](#server-architecture)
17 | - [Getting Started](#getting-started)
18 | - [Development Roadmap](#development-roadmap)
19 | - [Contributing](#contributing)
20 | - [License](#license)
21 | - [Notes](#notes)
22 |
23 | ## Screenshots
24 |
25 | Manage Ollama models easily with an intuitive interface and chat with them in real-time.
26 |
27 | | Main Interface | Chat Interface |
28 | |----------------|----------------|
29 | |  |  |
30 |
31 | ## Key Features
32 |
33 | - View installed model list
34 | - Start/stop models
35 | - Real-time chat
36 | - Model status monitoring
37 |
38 | ## Project Structure
39 |
40 | ```
41 | src/
42 | ├── client-opensilver/ # OpenSilver web client
43 | ├── client-wpf/ # WPF desktop client
44 | └── server-minimalapi/ # Shared backend server
45 | ```
46 |
47 | ## Tech Stack
48 |
49 | - **Web Client**: OpenSilver (.NET Standard 2.0)
50 | - **Desktop Client**: WPF (.NET 9.0)
51 | - **Backend**: ASP.NET Core Minimal API (.NET 9.0)
52 | - **Real-time Communication**: SignalR
53 |
54 | ## Server Architecture
55 |
56 | ### Minimal API Structure
57 | - **GET** `/api/models` - Retrieve installed models list and status
58 | - **POST** `/api/models/{modelName}/start` - Start model
59 | - **POST** `/api/models/{modelName}/stop` - Stop model
60 | - **POST** `/api/chat` - Chat with model
61 |
62 | ### Ollama API Integration
63 | The backend server manages models using the Ollama API:
64 | - `http://localhost:11434/api/tags` - Installed models list
65 | - `http://localhost:11434/api/ps` - Check running models
66 | - `http://localhost:11434/api/generate` - Model load/unload and chat
67 |
68 | ### Real-time Monitoring
69 | - Real-time model status updates via SignalR Hub
70 | - Background service for detecting model status changes
71 |
72 | ## Getting Started
73 |
74 | ### Prerequisites
75 | - **Ollama**: Install from [ollama.com](https://ollama.com)
76 | - **Model Installation**: Example: run `ollama pull llama3.2`
77 | - **Visual Studio 2022**
78 | - **.NET 9.0 SDK**
79 | - **WASM Tools**: `dotnet workload install wasm-tools`
80 | - **OpenSilver SDK**: Download `OpenSilver_SDK_v3.2.0.4.vsix` from [www.opensilver.net](https://www.opensilver.net) and install
81 |
82 | ### Server Execution
83 |
84 | First, run the backend server:
85 | ```bash
86 | cd src/server-minimalapi/LocalLLMServer
87 | dotnet run --launch-profile https
88 | ```
89 | The server will run at `https://localhost:7262`.
90 |
91 | ### OpenSilver Web Client
92 |
93 | ```bash
94 | cd src/client-opensilver/OllamaHub.Browser
95 | dotnet run
96 | ```
97 |
98 | Access via browser at `http://localhost:55592`
99 |
100 | ### WPF Version
101 |
102 | You can also run the WPF version using the same server:
103 | ```bash
104 | cd src/client-wpf/OllamaHub
105 | dotnet run
106 | ```
107 |
108 | ## Development Roadmap
109 |
110 | ### Currently Implemented
111 | - Model list retrieval
112 | - Model control (start/stop)
113 | - Real-time chat
114 |
115 | ### Planned Features
116 | - Model downloads
117 | - Model deletion
118 | - Multi-session chat
119 | - Performance monitoring
120 |
121 | ## Contributing
122 |
123 | 1. Fork the repository
124 | 2. Create a feature branch
125 | 3. Commit your changes
126 | 4. Create a Pull Request
127 |
128 | ## License
129 |
130 | MIT License
131 |
132 | ## Notes
133 |
134 | - This is an excellent example for verifying code compatibility between WPF and OpenSilver
135 | - OpenSilver is based on .NET Standard 2.0 and provides a WPF-like development experience on the web
136 |
--------------------------------------------------------------------------------
/src/client-wpf/OllamaHub.Support/Themes/Units/AIMessageListBoxItem.xaml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
82 |
83 |
--------------------------------------------------------------------------------
/src/client-opensilver/OllamaHub.Support/Themes/Units/ApplicationHeader.xaml:
--------------------------------------------------------------------------------
1 |
5 |
6 |
74 |
--------------------------------------------------------------------------------
/src/client-wpf/OllamaHub.Support/UI/Units/GlobalStatusBadge.cs:
--------------------------------------------------------------------------------
1 | using System.Windows;
2 | using System.Windows.Controls;
3 |
4 | namespace OllamaHub.Support.UI.Units;
5 |
6 | public class GlobalStatusBadge : Control
7 | {
8 | public GlobalStatusBadge()
9 | {
10 | DefaultStyleKey = typeof(GlobalStatusBadge);
11 | }
12 |
13 | public static readonly DependencyProperty CornerRadiusProperty =
14 | DependencyProperty.Register(
15 | "CornerRadius",
16 | typeof(CornerRadius),
17 | typeof(GlobalStatusBadge),
18 | new PropertyMetadata(new CornerRadius(0)));
19 |
20 |
21 |
22 | public static readonly DependencyProperty StatusProperty =
23 | DependencyProperty.Register(
24 | nameof(Status),
25 | typeof(GlobalStatus),
26 | typeof(GlobalStatusBadge),
27 | new PropertyMetadata(GlobalStatus.NoModelsRunning, OnStatusChanged));
28 |
29 | public CornerRadius CornerRadius
30 | {
31 | get { return (CornerRadius)GetValue(CornerRadiusProperty); }
32 | set { SetValue(CornerRadiusProperty, value); }
33 | }
34 |
35 | public GlobalStatus Status
36 | {
37 | get => (GlobalStatus)GetValue(StatusProperty);
38 | set => SetValue(StatusProperty, value);
39 | }
40 |
41 | public static readonly DependencyProperty ModelNameProperty =
42 | DependencyProperty.Register(
43 | nameof(ModelName),
44 | typeof(string),
45 | typeof(GlobalStatusBadge),
46 | new PropertyMetadata(string.Empty, OnModelNameChanged));
47 |
48 | public string ModelName
49 | {
50 | get => (string)GetValue(ModelNameProperty);
51 | set => SetValue(ModelNameProperty, value);
52 | }
53 |
54 | public static readonly DependencyProperty RunningCountProperty =
55 | DependencyProperty.Register(
56 | nameof(RunningCount),
57 | typeof(int),
58 | typeof(GlobalStatusBadge),
59 | new PropertyMetadata(0, OnRunningCountChanged));
60 |
61 | public int RunningCount
62 | {
63 | get => (int)GetValue(RunningCountProperty);
64 | set => SetValue(RunningCountProperty, value);
65 | }
66 |
67 | private static void OnStatusChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
68 | {
69 | if (d is GlobalStatusBadge badge)
70 | {
71 | badge.UpdateVisualState();
72 | }
73 | }
74 |
75 | private static void OnModelNameChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
76 | {
77 | if (d is GlobalStatusBadge badge)
78 | {
79 | badge.UpdateVisualState();
80 | }
81 | }
82 |
83 | private static void OnRunningCountChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
84 | {
85 | if (d is GlobalStatusBadge badge)
86 | {
87 | badge.UpdateVisualState();
88 | }
89 | }
90 |
91 | private void UpdateVisualState()
92 | {
93 | var state = Status.ToString();
94 | VisualStateManager.GoToState(this, state, true);
95 |
96 | UpdateStatusText();
97 | }
98 |
99 | private void UpdateStatusText()
100 | {
101 | if (GetTemplateChild("StatusText") is TextBlock statusText)
102 | {
103 | switch (Status)
104 | {
105 | case GlobalStatus.NoModelsRunning:
106 | statusText.Text = "No models running";
107 | break;
108 | case GlobalStatus.SingleModelRunning:
109 | statusText.Text = string.IsNullOrEmpty(ModelName) ? "Model running" : $"{ModelName} Running";
110 | break;
111 | case GlobalStatus.MultipleModelsRunning:
112 | statusText.Text = $"{RunningCount} models running";
113 | break;
114 | case GlobalStatus.SystemLoading:
115 | statusText.Text = "System loading";
116 | break;
117 | case GlobalStatus.SystemError:
118 | statusText.Text = "System error";
119 | break;
120 | case GlobalStatus.SystemIdle:
121 | statusText.Text = "System idle";
122 | break;
123 | case GlobalStatus.AllModelsDownloading:
124 | statusText.Text = "Downloading models";
125 | break;
126 | }
127 | }
128 | }
129 |
130 | public override void OnApplyTemplate()
131 | {
132 | base.OnApplyTemplate();
133 | UpdateVisualState();
134 | UpdateStatusText();
135 | }
136 | }
137 |
--------------------------------------------------------------------------------
/src/client-opensilver/OllamaHub.Support/OllamaHub.Support.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netstandard2.0
5 | false
6 | true
7 | 12
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 | Designer
50 | MSBuild:Compile
51 |
52 |
53 | Designer
54 | MSBuild:Compile
55 |
56 |
57 | Designer
58 | MSBuild:Compile
59 |
60 |
61 | Designer
62 | MSBuild:Compile
63 |
64 |
65 | Designer
66 | MSBuild:Compile
67 |
68 |
69 | Designer
70 | MSBuild:Compile
71 |
72 |
73 | Designer
74 | MSBuild:Compile
75 |
76 |
77 | Designer
78 | MSBuild:Compile
79 |
80 |
81 | Designer
82 | MSBuild:Compile
83 |
84 |
85 | Designer
86 | MSBuild:Compile
87 |
88 |
89 | Designer
90 | MSBuild:Compile
91 |
92 |
93 | Designer
94 | MSBuild:Compile
95 |
96 |
97 | Designer
98 | MSBuild:Compile
99 |
100 |
101 | Designer
102 | MSBuild:Compile
103 |
104 |
105 | MSBuild:Compile
106 |
107 |
108 |
109 |
110 |
--------------------------------------------------------------------------------
/src/client-opensilver/OllamaHub.Support/UI/Units/GlobalStatusBadge.cs:
--------------------------------------------------------------------------------
1 | using System.Windows;
2 | using System.Windows.Controls;
3 |
4 | namespace OllamaHub.Support.UI.Units;
5 |
6 | public class GlobalStatusBadge : Control
7 | {
8 | public GlobalStatusBadge()
9 | {
10 | DefaultStyleKey = typeof(GlobalStatusBadge);
11 | }
12 |
13 | public static readonly DependencyProperty CornerRadiusProperty =
14 | DependencyProperty.Register(
15 | "CornerRadius",
16 | typeof(CornerRadius),
17 | typeof(GlobalStatusBadge),
18 | new PropertyMetadata(new CornerRadius(0)));
19 |
20 |
21 |
22 | public static readonly DependencyProperty StatusProperty =
23 | DependencyProperty.Register(
24 | nameof(Status),
25 | typeof(GlobalStatus),
26 | typeof(GlobalStatusBadge),
27 | new PropertyMetadata(GlobalStatus.NoModelsRunning, OnStatusChanged));
28 |
29 | public CornerRadius CornerRadius
30 | {
31 | get { return (CornerRadius)GetValue(CornerRadiusProperty); }
32 | set { SetValue(CornerRadiusProperty, value); }
33 | }
34 |
35 | public GlobalStatus Status
36 | {
37 | get => (GlobalStatus)GetValue(StatusProperty);
38 | set => SetValue(StatusProperty, value);
39 | }
40 |
41 | public static readonly DependencyProperty ModelNameProperty =
42 | DependencyProperty.Register(
43 | nameof(ModelName),
44 | typeof(string),
45 | typeof(GlobalStatusBadge),
46 | new PropertyMetadata(string.Empty, OnModelNameChanged));
47 |
48 | public string ModelName
49 | {
50 | get => (string)GetValue(ModelNameProperty);
51 | set => SetValue(ModelNameProperty, value);
52 | }
53 |
54 | public static readonly DependencyProperty RunningCountProperty =
55 | DependencyProperty.Register(
56 | nameof(RunningCount),
57 | typeof(int),
58 | typeof(GlobalStatusBadge),
59 | new PropertyMetadata(0, OnRunningCountChanged));
60 |
61 | public int RunningCount
62 | {
63 | get => (int)GetValue(RunningCountProperty);
64 | set => SetValue(RunningCountProperty, value);
65 | }
66 |
67 | private static void OnStatusChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
68 | {
69 | if (d is GlobalStatusBadge badge)
70 | {
71 | badge.UpdateVisualState();
72 | }
73 | }
74 |
75 | private static void OnModelNameChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
76 | {
77 | if (d is GlobalStatusBadge badge)
78 | {
79 | badge.UpdateVisualState();
80 | }
81 | }
82 |
83 | private static void OnRunningCountChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
84 | {
85 | if (d is GlobalStatusBadge badge)
86 | {
87 | badge.UpdateVisualState();
88 | }
89 | }
90 |
91 | private void UpdateVisualState()
92 | {
93 | var state = Status.ToString();
94 | VisualStateManager.GoToState(this, state, true);
95 |
96 | UpdateStatusText();
97 | }
98 |
99 | private void UpdateStatusText()
100 | {
101 | if (GetTemplateChild("StatusText") is TextBlock statusText)
102 | {
103 | switch (Status)
104 | {
105 | case GlobalStatus.NoModelsRunning:
106 | statusText.Text = "No models running";
107 | break;
108 | case GlobalStatus.SingleModelRunning:
109 | statusText.Text = string.IsNullOrEmpty(ModelName) ? "Model running" : $"{ModelName} Running";
110 | break;
111 | case GlobalStatus.MultipleModelsRunning:
112 | statusText.Text = $"{RunningCount} models running";
113 | break;
114 | case GlobalStatus.SystemLoading:
115 | statusText.Text = "System loading";
116 | break;
117 | case GlobalStatus.SystemError:
118 | statusText.Text = "System error";
119 | break;
120 | case GlobalStatus.SystemIdle:
121 | statusText.Text = "System idle";
122 | break;
123 | case GlobalStatus.AllModelsDownloading:
124 | statusText.Text = "Downloading models";
125 | break;
126 | }
127 | }
128 | }
129 |
130 | public override void OnApplyTemplate()
131 | {
132 | base.OnApplyTemplate();
133 | UpdateVisualState();
134 | UpdateStatusText();
135 | }
136 | }
137 |
--------------------------------------------------------------------------------
/src/client-opensilver/OllamaHub.Support/Themes/Units/NavigationRadioButton.xaml:
--------------------------------------------------------------------------------
1 |
5 |
6 |
71 |
--------------------------------------------------------------------------------
/src/client-opensilver/OllamaHub.Browser/wwwroot/modern/loading-animation.js:
--------------------------------------------------------------------------------
1 | (function () {
2 | let lastAnimationTime = 0;
3 | const ANIMATION_THROTTLE_DELAY = 150; // Minimum time between animations
4 | const ANIMATION_DURATION = 150; // Total animation duration
5 |
6 | const odometerClass = "opensilver-odometer";
7 |
8 | function startLoader() {
9 | const count = document.querySelectorAll("." + odometerClass);
10 | const loader = document.querySelector(".opensilver-loader-progress-bar");
11 | const loaderProgress = document.querySelector(".opensilver-loader-progress");
12 | if (!count || !loader) return;
13 |
14 | loader.style.width = "0%";
15 | const observer = new MutationObserver(updateCount);
16 |
17 | function updateCount() {
18 | const loadPercentageText = getComputedStyle(document.documentElement)
19 | .getPropertyValue("--blazor-load-percentage-text")
20 | .trim();
21 | const loadPercentage = parseInt(loadPercentageText.replace(/"/g, ""));
22 | const currentValue = isNaN(loadPercentage) ? 0 : loadPercentage;
23 |
24 | // Always animate 100 regardless of throttling
25 | if (currentValue === 100) {
26 | animateCounter(currentValue, true);
27 | loader.style.width = "100%";
28 | loaderProgress.style["border-right"] = "none";
29 | observer.disconnect();
30 | return;
31 | }
32 |
33 | // Throttle animations to prevent too frequent updates
34 | const now = Date.now();
35 | if (now - lastAnimationTime >= ANIMATION_THROTTLE_DELAY) {
36 | animateCounter(currentValue);
37 | lastAnimationTime = now;
38 | }
39 |
40 | loader.style.width = currentValue + "%";
41 | }
42 |
43 | observer.observe(document.documentElement, {
44 | attributes: true,
45 | attributeFilter: ["style"],
46 | });
47 | updateCount();
48 | }
49 |
50 | function animateCounter(newValue, force = false) {
51 | const odometers = Array.from(document.querySelectorAll("." + odometerClass));
52 | const newValueString = String(newValue).padStart(3, "0");
53 |
54 | for (let index = odometers.length - 1; index > -1; index--) {
55 | const element = odometers[index];
56 |
57 | if (force) {
58 | const finalOdometer = document.createElement("div");
59 | finalOdometer.textContent = newValueString[index];
60 | finalOdometer.classList.add(odometerClass);
61 | element.replaceWith(finalOdometer);
62 | continue;
63 | }
64 |
65 | if (element.textContent === "") {
66 | element.textContent = "0";
67 | }
68 | const currentValue = element.textContent || "0";
69 |
70 | if (newValueString[index] !== currentValue) {
71 | element.style.transition = `transform ${ANIMATION_DURATION}ms, opacity ${ANIMATION_DURATION}ms`;
72 | element.style.transform = "translateY(-4px)";
73 | element.style.opacity = "0.5";
74 |
75 | setTimeout(() => {
76 | element.textContent = newValueString[index];
77 | element.style.transform = "translateY(4px)";
78 | element.style.opacity = "0.5";
79 |
80 | setTimeout(() => {
81 | element.style.transform = "translateY(0)";
82 | element.style.opacity = "1";
83 | }, ANIMATION_DURATION / 2);
84 | }, ANIMATION_DURATION / 2);
85 | }
86 | };
87 | }
88 |
89 | function onDomReady() {
90 | startLoader();
91 | startAnimations();
92 | }
93 |
94 | function startAnimations() {
95 | const loaderProgress = document.querySelector(".opensilver-loader-progress");
96 | const counterContainer = document.querySelector(".opensilver-counter-container");
97 |
98 | if (loaderProgress) {
99 | loaderProgress.style.transition = "width 1.25s, opacity 1.25s";
100 | loaderProgress.style.width = "60vw";
101 | loaderProgress.style.opacity = "1";
102 | }
103 |
104 | if (counterContainer) {
105 | counterContainer.style.transition = "opacity 0.3s";
106 | counterContainer.style.opacity = "1";
107 | }
108 | }
109 |
110 | if (document.readyState === 'loading') {
111 | document.addEventListener('DOMContentLoaded', onDomReady);
112 | } else {
113 | onDomReady();
114 | }
115 | })();
--------------------------------------------------------------------------------
/src/client-wpf/OllamaHub.Support/Themes/Units/ChatListBox.xaml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 |
92 |
93 |
--------------------------------------------------------------------------------
/src/client-opensilver/OllamaHub.Support/Themes/Units/AIMessageListBoxItem.xaml:
--------------------------------------------------------------------------------
1 |
5 |
6 |
75 |
76 |
--------------------------------------------------------------------------------
/src/client-wpf/OllamaHub.Support/Themes/Units/ApplicationHeader.xaml:
--------------------------------------------------------------------------------
1 |
5 |
6 |
95 |
--------------------------------------------------------------------------------
/src/client-opensilver/OllamaHub.Browser/wwwroot/libs/cshtml5.css:
--------------------------------------------------------------------------------
1 |
2 | /*===================================================================================
3 | *
4 | * Copyright (c) Userware (OpenSilver.net, CSHTML5.com)
5 | *
6 | * This file is part of both the OpenSilver Runtime (https://opensilver.net), which
7 | * is licensed under the MIT license (https://opensource.org/licenses/MIT), and the
8 | * CSHTML5 Runtime (http://cshtml5.com), which is dual-licensed (MIT + commercial).
9 | *
10 | * As stated in the MIT license, "the above copyright notice and this permission
11 | * notice shall be included in all copies or substantial portions of the Software."
12 | *
13 | \*====================================================================================*/
14 |
15 | * {
16 | -webkit-tap-highlight-color: rgba(0,0,0,0);
17 | }
18 |
19 | html {
20 | height: 100%;
21 | width: 100%;
22 | margin: 0px;
23 | }
24 |
25 | body {
26 | background-color: white;
27 | margin: 0px;
28 | padding: 0px;
29 | height: 100%;
30 | width: 100%;
31 | font-size: 11px;
32 | overflow-x: hidden;
33 | overflow-y: hidden;
34 | cursor: default;
35 | font-family: 'Segoe UI', Verdana, 'DejaVu Sans', Lucida, 'MS Sans Serif', sans-serif;
36 | -webkit-touch-callout: none; /* prevents callout to copy image, etc when tap to hold */
37 | -webkit-text-size-adjust: none; /* prevents webkit from resizing text to fit */
38 | -webkit-user-select: text; /* 'none' prevents copy paste. 'text' allows it. */
39 | }
40 |
41 | .opensilver-pointer-captured {
42 | -moz-user-select: none; /* Firefox */
43 | -webkit-user-select: none; /* Chrome, Safari, and Opera */
44 | -ms-user-select: none; /* Internet Explorer/Edge */
45 | user-select: none;
46 | }
47 |
48 | .opensilver-root-element {
49 | touch-action: none; /* prevents the browser from cancelling pointermove events */
50 | }
51 |
52 | .uielement-collapsed {
53 | display: none !important;
54 | }
55 |
56 | .uielement-unarranged {
57 | opacity: 0 !important;
58 | }
59 |
60 | .opensilver-hyperlink {
61 | cursor: pointer;
62 | }
63 |
64 | .opensilver-hyperlink:hover {
65 | color: var(--mouse-over-color, rgb(237, 110, 0)) !important;
66 | text-decoration: var(--mouse-over-decoration, underline) !important;
67 | }
68 |
69 | .opensilver-uielement {
70 | position: absolute;
71 | box-sizing: border-box;
72 | pointer-events: none;
73 | z-index: 0;
74 | outline: none;
75 | transform-origin: 0% 0%;
76 | }
77 |
78 | .opensilver-shape {
79 | pointer-events: none !important;
80 | fill: none; /* CSS default value is black */
81 | fill-rule: evenodd; /* CSS default value is nonzero, and evenodd is the default value of every shape */
82 | stroke: none; /* CSS default value is black */
83 | stroke-miterlimit: 10; /* CSS default value is 4 */
84 | }
85 |
86 | .opensilver-border {
87 | border-style: solid;
88 | border-color: transparent; /* CSS default value matches the color property (currentcolor) */
89 | border-width: 0px; /* CSS default value is medium */
90 | background-clip: padding-box !important;
91 | }
92 |
93 | .opensilver-window {
94 | width: 100%;
95 | height: 100%;
96 | overflow: hidden;
97 | transform-origin: 0% 0%;
98 | }
99 |
100 | .opensilver-popup {
101 | position: absolute;
102 | width: 100%;
103 | height: 100%;
104 | overflow: clip;
105 | z-index: 2147483647;
106 | }
107 |
108 | .opensilver-inkpresenter {
109 | width: 100%;
110 | height: 100%;
111 | position: absolute;
112 | pointer-events: none;
113 | }
114 |
115 | .opensilver-textblock {
116 | text-overflow: ellipsis;
117 | text-align: start;
118 | white-space: pre;
119 | overflow: visible;
120 | }
121 |
122 | .opensilver-inline {
123 | display: inline;
124 | line-height: var(--line-stacking-strategy, normal);
125 | }
126 |
127 | .opensilver-block {
128 | display: block;
129 | box-sizing: border-box;
130 | border-style: solid;
131 | border-color: transparent; /* CSS default value matches the color property (currentcolor) */
132 | border-width: 0px; /* CSS default value is medium */
133 | background-clip: padding-box !important;
134 | }
135 |
136 | .opensilver-textboxview {
137 | font-size: inherit;
138 | font-family: inherit;
139 | color: inherit;
140 | letter-spacing: inherit;
141 | border: none;
142 | background: transparent;
143 | padding: 0;
144 | resize: none;
145 | cursor: text;
146 | overflow: hidden;
147 | tab-size: 4;
148 | }
149 |
150 | .opensilver-textboxview:focus::selection {
151 | color: var(--selection-color, HighlightText);
152 | background-color: var(--selection-bg-color, Highlight);
153 | }
154 |
155 | .opensilver-passwordboxview {
156 | font-size: inherit;
157 | font-family: inherit;
158 | color: inherit;
159 | letter-spacing: inherit;
160 | border: none;
161 | background: transparent;
162 | padding: 0;
163 | }
164 |
165 | .opensilver-passwordboxview:focus::selection {
166 | color: var(--selection-color, HighlightText);
167 | background-color: var(--selection-bg-color, Highlight);
168 | }
169 |
170 | /* Media query used for printing (cf. "CSHTML5.Native.Html.Printing.PrintManager") */
171 | @media print {
172 | body {
173 | -webkit-print-color-adjust: exact;
174 | }
175 |
176 | body * {
177 | visibility: hidden;
178 | }
179 |
180 | .section-to-print, .section-to-print * {
181 | visibility: visible;
182 | }
183 |
184 | .section-to-print {
185 | position: fixed;
186 | left: 0;
187 | top: 0;
188 | height: 100%;
189 | width: 100%;
190 | }
191 | }
192 |
--------------------------------------------------------------------------------
/src/client-wpf/OllamaHub.Support/Themes/Units/NavigationRadioButton.xaml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
88 |
89 |
--------------------------------------------------------------------------------
/src/client-wpf/OllamaHub.Main/Local/ViewModels/MainViewModel.cs:
--------------------------------------------------------------------------------
1 | using Jamesnet.Foundation;
2 | using Microsoft.AspNetCore.SignalR.Client;
3 | using OllamaHub.Support.Local.Models;
4 | using OllamaHub.Support.Local.Services;
5 | using System.Collections.ObjectModel;
6 | using System.Windows.Input;
7 | using System.Linq;
8 | using System.Windows;
9 |
10 | namespace OllamaHub.Main.Local.ViewModels;
11 |
12 | public partial class MainViewModel : ViewModelBase
13 | {
14 | private readonly ApiClient _apiClient;
15 | private readonly HubConnection _hubConnection;
16 |
17 | private ObservableCollection _models;
18 | private ObservableCollection _runningModels;
19 | private bool _isLoading;
20 | private ObservableCollection